import { ApprovalRequestId, EnvironmentId, ThreadId } from "@t3tools/contracts";import { describe, expect, it, vi } from "vite-plus/test";import { questionAttachmentDraftKey, questionAttachmentDraftPrefix,} from "../state/question-attachments";vi.mock("../state/atom-registry", () => ({ appAtomRegistry: {} })); import { composerAttachmentUploadBlockReason, composerAttachmentsStillUploading, composerAttachmentUploadKey, composerDraftEnvironmentId, createComposerAttachmentUploadQueue, type ComposerAttachmentUploadRequest, type ComposerAttachmentUploadState,} from "./composerAttachmentUploadQueue"; const environmentId = EnvironmentId.make("environment-1");function request(id: string, environment = environmentId): ComposerAttachmentUploadRequest { return { environmentId: environment, attachment: { id, type: "file", name: `${id}.pdf`, mimeType: "application/pdf", sizeBytes: 42, fileUri: `file:///documents/${id}.pdf`, }, };} describe("composer attachment upload queue", () => { it("bounds concurrency, deduplicates updates, and drains all attachments", async () => { const gates = new Map<string, ReturnType<typeof Promise.withResolvers<boolean>>>(); const fourthStarted = Promise.withResolvers<void>(); const firstThreeStarted = Promise.withResolvers<void>(); let active = 0; let maximum = 0; const upload = vi.fn(async (input: ComposerAttachmentUploadRequest) => { active += 1; maximum = Math.max(maximum, active); const gate = Promise.withResolvers<boolean>(); gates.set(input.attachment.id, gate); if (gates.size === 3) firstThreeStarted.resolve(); if (gates.size === 4) fourthStarted.resolve(); try { return await gate.promise; } finally { active -= 1; } }); const queue = createComposerAttachmentUploadQueue({ upload, onChange: () => {} }); const requests = [request("one"), request("two"), request("three"), request("four")]; queue.sync(requests); queue.sync(requests); await firstThreeStarted.promise; expect(upload).toHaveBeenCalledTimes(3); gates.get("one")!.resolve(true); await fourthStarted.promise; for (const gate of gates.values()) gate.resolve(true); await queue.settled(); queue.sync(requests); await queue.settled(); expect(maximum).toBe(3); expect(upload).toHaveBeenCalledTimes(4); queue.dispose(); }); it("cancels on disconnect and resumes from the same local draft on reconnect", async () => { const started = Promise.withResolvers<void>(); let states: Readonly<Record<string, ComposerAttachmentUploadState>> = {}; let signal: AbortSignal | undefined; const upload = vi.fn( async (_request: ComposerAttachmentUploadRequest, currentSignal: AbortSignal) => { signal = currentSignal; started.resolve(); return new Promise<boolean>((resolve) => currentSignal.addEventListener("abort", () => resolve(false), { once: true }), ); }, ); const queue = createComposerAttachmentUploadQueue({ upload, onChange: (next) => { states = next; }, }); const local = request("offline-draft"); queue.sync([local]); await started.promise; queue.sync([]); await queue.settled(); expect(signal?.aborted).toBe(true); expect(states).toEqual({}); upload.mockResolvedValueOnce(true); queue.sync([local]); await queue.settled(); expect(upload.mock.calls[1]?.[0]).toBe(local); expect(states[composerAttachmentUploadKey(environmentId, local.attachment.id)]).toEqual({ status: "ready", }); expect(local.attachment).toMatchObject({ fileUri: "file:///documents/offline-draft.pdf" }); queue.dispose(); }); it("ignores a late completion after removal or environment switch", async () => { const gate = Promise.withResolvers<boolean>(); const started = Promise.withResolvers<void>(); let states: Readonly<Record<string, ComposerAttachmentUploadState>> = {}; const upload = vi.fn(async () => { started.resolve(); return gate.promise; }); const queue = createComposerAttachmentUploadQueue({ upload, onChange: (next) => { states = next; }, }); queue.sync([request("photo")]); await started.promise; upload.mockResolvedValueOnce(true); const other = EnvironmentId.make("environment-2"); queue.sync([request("photo", other)]); gate.resolve(true); await queue.settled(); expect(states).toEqual({ [composerAttachmentUploadKey(other, "photo")]: { status: "ready" } }); queue.sync([]); expect(states).toEqual({}); queue.dispose(); }); it("restarts a re-added attachment after its aborted transfer finishes settling", async () => { const firstStarted = Promise.withResolvers<void>(); const firstSettled = Promise.withResolvers<boolean>(); const secondStarted = Promise.withResolvers<void>(); const secondSettled = Promise.withResolvers<boolean>(); let states: Readonly<Record<string, ComposerAttachmentUploadState>> = {}; let firstSignal: AbortSignal | undefined; const upload = vi.fn(async (_request: ComposerAttachmentUploadRequest, signal: AbortSignal) => { if (!firstSignal) { firstSignal = signal; firstStarted.resolve(); return firstSettled.promise; } secondStarted.resolve(); return secondSettled.promise; }); const queue = createComposerAttachmentUploadQueue({ upload, onChange: (next) => { states = next; }, }); const local = request("re-added"); queue.sync([local]); await firstStarted.promise; queue.sync([]); queue.sync([local]); expect(firstSignal?.aborted).toBe(true); expect(upload).toHaveBeenCalledOnce(); firstSettled.resolve(false); await secondStarted.promise; expect(upload).toHaveBeenCalledTimes(2); secondSettled.resolve(true); await queue.settled(); expect(states[composerAttachmentUploadKey(environmentId, local.attachment.id)]).toEqual({ status: "ready", }); queue.dispose(); }); it("keeps failures stable until retry and reports bounded progress", async () => { let states: Readonly<Record<string, ComposerAttachmentUploadState>> = {}; const progress: number[] = []; const upload = vi.fn( async ( _request: ComposerAttachmentUploadRequest, _signal: AbortSignal, report: (value: number) => void, ): Promise<boolean> => { report(0.12); report(0.13); report(1.1); throw new Error("Server unavailable"); }, ); const queue = createComposerAttachmentUploadQueue({ upload, onChange: (next) => { states = next; const state = next[composerAttachmentUploadKey(environmentId, "file")]; if (state?.status === "uploading") progress.push(state.progress); }, }); queue.sync([request("file")]); await queue.settled(); queue.sync([request("file")]); expect(upload).toHaveBeenCalledOnce(); expect(states[composerAttachmentUploadKey(environmentId, "file")]).toEqual({ status: "failed", reason: "Server unavailable", }); expect(progress).toEqual([0, 0.1, 1]); upload.mockImplementationOnce(async () => true); queue.retry(environmentId, "file"); await queue.settled(); expect(states[composerAttachmentUploadKey(environmentId, "file")]).toEqual({ status: "ready" }); queue.dispose(); }); it("does not spin when an upload's draft was abandoned before persistence", async () => { const upload = vi.fn(async () => false); const queue = createComposerAttachmentUploadQueue({ upload, onChange: () => {} }); queue.sync([request("discarded")]); await queue.settled(); expect(upload).toHaveBeenCalledOnce(); queue.dispose(); });}); describe("draft upload scope and offline submission", () => { it("uploads question drafts in their environment without matching other thread prefixes", () => { const requestId = ApprovalRequestId.make("request:1"); for (const environment of [environmentId, EnvironmentId.make("remote:server")]) { const threadId = ThreadId.make("thread:1"); const prefix = questionAttachmentDraftPrefix(environment, threadId); const key = questionAttachmentDraftKey(environment, threadId, requestId, "question:1"); expect(composerDraftEnvironmentId(key, [])).toBe(environment); expect(key.startsWith(prefix)).toBe(true); for (const suffix of ["-extra", ":extra", "/extra", "%extra"]) { const otherKey = questionAttachmentDraftKey( environment, ThreadId.make(`${threadId}${suffix}`), requestId, "question:1", ); expect(otherKey.startsWith(prefix)).toBe(false); expect(composerDraftEnvironmentId(otherKey, [])).toBe(environment); } } }); it("resolves thread, new-task, and queued-task drafts without crossing environments", () => { expect(composerDraftEnvironmentId("environment-1:thread", [])).toBe(environmentId); expect(composerDraftEnvironmentId("new-task:environment-1:project", [])).toBe(environmentId); expect( composerDraftEnvironmentId("pending-task:message", [{ messageId: "message", environmentId }]), ).toBe(environmentId); expect(composerDraftEnvironmentId("pending-task:missing", [])).toBeNull(); const colonEnvironment = EnvironmentId.make("a:vcs-status:b"); expect(composerDraftEnvironmentId(`${colonEnvironment}:thread`, [])).toBe(colonEnvironment); expect(composerDraftEnvironmentId(`new-task:${colonEnvironment}:project`, [])).toBe( colonEnvironment, ); }); it("reads an id-keyed new-task draft's environment from its project stamp", () => { const stamped = { project: { environmentId, projectId: "project" as never, createdAt: "2026-09-05T00:00:00.000Z", }, }; expect(composerDraftEnvironmentId("new-task:abc123-def456", [], stamped)).toBe(environmentId); // An id-keyed draft that lost its stamp belongs to nobody: uploads must // not start and sign-out must not sweep it into some other environment. expect(composerDraftEnvironmentId("new-task:abc123-def456", [])).toBeNull(); // The stamp wins over a legacy-looking key when both are present. expect(composerDraftEnvironmentId("new-task:environment-2:project", [], stamped)).toBe( environmentId, ); }); it("only a failed upload blocks sending; an in-flight one queues instead", () => { const key = composerAttachmentUploadKey(environmentId, "file"); const input = { environmentId, attachments: [request("file").attachment], connected: true, serverConfig: { environment: { capabilities: { attachmentUploads: true, fileAttachments: { maxUploadBytes: 1024 } }, }, }, states: {}, }; // Not started yet and mid-transfer both let the send through as a queued // message; the outbox drain finishes (or redoes) the upload. expect(composerAttachmentUploadBlockReason(input)).toBeNull(); expect(composerAttachmentsStillUploading(input)).toBe(true); expect( composerAttachmentsStillUploading({ ...input, states: { [key]: { status: "uploading", progress: 0.6 } }, }), ).toBe(true); expect(composerAttachmentUploadBlockReason({ ...input, connected: false })).toBeNull(); expect( composerAttachmentUploadBlockReason({ ...input, states: { [key]: { status: "failed", reason: "Offline" } }, }), ).toBe("Retry or remove the failed attachment"); expect( composerAttachmentsStillUploading({ ...input, states: { [key]: { status: "failed", reason: "Offline" } }, }), ).toBe(false); expect( composerAttachmentUploadBlockReason({ ...input, states: { [key]: { status: "ready" } } }), ).toBeNull(); expect( composerAttachmentsStillUploading({ ...input, states: { [key]: { status: "ready" } } }), ).toBe(false); // Attachments the environment cannot accept never count as uploading. expect(composerAttachmentsStillUploading({ ...input, serverConfig: null })).toBe(false); });});
import { ApprovalRequestId, EnvironmentId, ThreadId } from "@t3tools/contracts";
import { describe, expect, it, vi } from "vite-plus/test";
import {
questionAttachmentDraftKey,
questionAttachmentDraftPrefix,
} from "../state/question-attachments";
vi.mock("../state/atom-registry", () => ({ appAtomRegistry: {} }));
import {
composerAttachmentUploadBlockReason,
composerAttachmentsStillUploading,
composerAttachmentUploadKey,
composerDraftEnvironmentId,
createComposerAttachmentUploadQueue,
type ComposerAttachmentUploadRequest,
type ComposerAttachmentUploadState,
} from "./composerAttachmentUploadQueue";
const environmentId = EnvironmentId.make("environment-1");
function request(id: string, environment = environmentId): ComposerAttachmentUploadRequest {
return {
environmentId: environment,
attachment: {
id,
type: "file",
name: `${id}.pdf`,
mimeType: "application/pdf",
sizeBytes: 42,
fileUri: `file:///documents/${id}.pdf`,
},
};
}
describe("composer attachment upload queue", () => {
it("bounds concurrency, deduplicates updates, and drains all attachments", async () => {
const gates = new Map<string, ReturnType<typeof Promise.withResolvers<boolean>>>();
const fourthStarted = Promise.withResolvers<void>();
const firstThreeStarted = Promise.withResolvers<void>();
let active = 0;
let maximum = 0;
const upload = vi.fn(async (input: ComposerAttachmentUploadRequest) => {
active += 1;
maximum = Math.max(maximum, active);
const gate = Promise.withResolvers<boolean>();
gates.set(input.attachment.id, gate);
if (gates.size === 3) firstThreeStarted.resolve();
if (gates.size === 4) fourthStarted.resolve();
try {
return await gate.promise;
} finally {
active -= 1;
}
});
const queue = createComposerAttachmentUploadQueue({ upload, onChange: () => {} });
const requests = [request("one"), request("two"), request("three"), request("four")];
queue.sync(requests);
queue.sync(requests);
await firstThreeStarted.promise;
expect(upload).toHaveBeenCalledTimes(3);
gates.get("one")!.resolve(true);
await fourthStarted.promise;
for (const gate of gates.values()) gate.resolve(true);
await queue.settled();
queue.sync(requests);
await queue.settled();
expect(maximum).toBe(3);
expect(upload).toHaveBeenCalledTimes(4);
queue.dispose();
});
it("cancels on disconnect and resumes from the same local draft on reconnect", async () => {
const started = Promise.withResolvers<void>();
let states: Readonly<Record<string, ComposerAttachmentUploadState>> = {};
let signal: AbortSignal | undefined;
const upload = vi.fn(
async (_request: ComposerAttachmentUploadRequest, currentSignal: AbortSignal) => {
signal = currentSignal;
started.resolve();
return new Promise<boolean>((resolve) =>
currentSignal.addEventListener("abort", () => resolve(false), { once: true }),
);
},
);
const queue = createComposerAttachmentUploadQueue({
upload,
onChange: (next) => {
states = next;
},
});
const local = request("offline-draft");
queue.sync([local]);
await started.promise;
queue.sync([]);
await queue.settled();
expect(signal?.aborted).toBe(true);
expect(states).toEqual({});
upload.mockResolvedValueOnce(true);
queue.sync([local]);
await queue.settled();
expect(upload.mock.calls[1]?.[0]).toBe(local);
expect(states[composerAttachmentUploadKey(environmentId, local.attachment.id)]).toEqual({
status: "ready",
});
expect(local.attachment).toMatchObject({ fileUri: "file:///documents/offline-draft.pdf" });
queue.dispose();
});
it("ignores a late completion after removal or environment switch", async () => {
const gate = Promise.withResolvers<boolean>();
const started = Promise.withResolvers<void>();
let states: Readonly<Record<string, ComposerAttachmentUploadState>> = {};
const upload = vi.fn(async () => {
started.resolve();
return gate.promise;
});
const queue = createComposerAttachmentUploadQueue({
upload,
onChange: (next) => {
states = next;
},
});
queue.sync([request("photo")]);
await started.promise;
upload.mockResolvedValueOnce(true);
const other = EnvironmentId.make("environment-2");
queue.sync([request("photo", other)]);
gate.resolve(true);
await queue.settled();
expect(states).toEqual({ [composerAttachmentUploadKey(other, "photo")]: { status: "ready" } });
queue.sync([]);
expect(states).toEqual({});
queue.dispose();
});
it("restarts a re-added attachment after its aborted transfer finishes settling", async () => {
const firstStarted = Promise.withResolvers<void>();
const firstSettled = Promise.withResolvers<boolean>();
const secondStarted = Promise.withResolvers<void>();
const secondSettled = Promise.withResolvers<boolean>();
let states: Readonly<Record<string, ComposerAttachmentUploadState>> = {};
let firstSignal: AbortSignal | undefined;
const upload = vi.fn(async (_request: ComposerAttachmentUploadRequest, signal: AbortSignal) => {
if (!firstSignal) {
firstSignal = signal;
firstStarted.resolve();
return firstSettled.promise;
}
secondStarted.resolve();
return secondSettled.promise;
});
const queue = createComposerAttachmentUploadQueue({
upload,
onChange: (next) => {
states = next;
},
});
const local = request("re-added");
queue.sync([local]);
await firstStarted.promise;
queue.sync([]);
queue.sync([local]);
expect(firstSignal?.aborted).toBe(true);
expect(upload).toHaveBeenCalledOnce();
firstSettled.resolve(false);
await secondStarted.promise;
expect(upload).toHaveBeenCalledTimes(2);
secondSettled.resolve(true);
await queue.settled();
expect(states[composerAttachmentUploadKey(environmentId, local.attachment.id)]).toEqual({
status: "ready",
});
queue.dispose();
});
it("keeps failures stable until retry and reports bounded progress", async () => {
let states: Readonly<Record<string, ComposerAttachmentUploadState>> = {};
const progress: number[] = [];
const upload = vi.fn(
async (
_request: ComposerAttachmentUploadRequest,
_signal: AbortSignal,
report: (value: number) => void,
): Promise<boolean> => {
report(0.12);
report(0.13);
report(1.1);
throw new Error("Server unavailable");
},
);
const queue = createComposerAttachmentUploadQueue({
upload,
onChange: (next) => {
states = next;
const state = next[composerAttachmentUploadKey(environmentId, "file")];
if (state?.status === "uploading") progress.push(state.progress);
},
});
queue.sync([request("file")]);
await queue.settled();
queue.sync([request("file")]);
expect(upload).toHaveBeenCalledOnce();
expect(states[composerAttachmentUploadKey(environmentId, "file")]).toEqual({
status: "failed",
reason: "Server unavailable",
});
expect(progress).toEqual([0, 0.1, 1]);
upload.mockImplementationOnce(async () => true);
queue.retry(environmentId, "file");
await queue.settled();
expect(states[composerAttachmentUploadKey(environmentId, "file")]).toEqual({ status: "ready" });
queue.dispose();
});
it("does not spin when an upload's draft was abandoned before persistence", async () => {
const upload = vi.fn(async () => false);
const queue = createComposerAttachmentUploadQueue({ upload, onChange: () => {} });
queue.sync([request("discarded")]);
await queue.settled();
expect(upload).toHaveBeenCalledOnce();
queue.dispose();
});
});
describe("draft upload scope and offline submission", () => {
it("uploads question drafts in their environment without matching other thread prefixes", () => {
const requestId = ApprovalRequestId.make("request:1");
for (const environment of [environmentId, EnvironmentId.make("remote:server")]) {
const threadId = ThreadId.make("thread:1");
const prefix = questionAttachmentDraftPrefix(environment, threadId);
const key = questionAttachmentDraftKey(environment, threadId, requestId, "question:1");
expect(composerDraftEnvironmentId(key, [])).toBe(environment);
expect(key.startsWith(prefix)).toBe(true);
for (const suffix of ["-extra", ":extra", "/extra", "%extra"]) {
const otherKey = questionAttachmentDraftKey(
environment,
ThreadId.make(`${threadId}${suffix}`),
requestId,
"question:1",
);
expect(otherKey.startsWith(prefix)).toBe(false);
expect(composerDraftEnvironmentId(otherKey, [])).toBe(environment);
}
}
});
it("resolves thread, new-task, and queued-task drafts without crossing environments", () => {
expect(composerDraftEnvironmentId("environment-1:thread", [])).toBe(environmentId);
expect(composerDraftEnvironmentId("new-task:environment-1:project", [])).toBe(environmentId);
expect(
composerDraftEnvironmentId("pending-task:message", [{ messageId: "message", environmentId }]),
).toBe(environmentId);
expect(composerDraftEnvironmentId("pending-task:missing", [])).toBeNull();
const colonEnvironment = EnvironmentId.make("a:vcs-status:b");
expect(composerDraftEnvironmentId(`${colonEnvironment}:thread`, [])).toBe(colonEnvironment);
expect(composerDraftEnvironmentId(`new-task:${colonEnvironment}:project`, [])).toBe(
colonEnvironment,
);
});
it("reads an id-keyed new-task draft's environment from its project stamp", () => {
const stamped = {
project: {
environmentId,
projectId: "project" as never,
createdAt: "2026-09-05T00:00:00.000Z",
},
};
expect(composerDraftEnvironmentId("new-task:abc123-def456", [], stamped)).toBe(environmentId);
// An id-keyed draft that lost its stamp belongs to nobody: uploads must
// not start and sign-out must not sweep it into some other environment.
expect(composerDraftEnvironmentId("new-task:abc123-def456", [])).toBeNull();
// The stamp wins over a legacy-looking key when both are present.
expect(composerDraftEnvironmentId("new-task:environment-2:project", [], stamped)).toBe(
environmentId,
);
});
it("only a failed upload blocks sending; an in-flight one queues instead", () => {
const key = composerAttachmentUploadKey(environmentId, "file");
const input = {
environmentId,
attachments: [request("file").attachment],
connected: true,
serverConfig: {
environment: {
capabilities: { attachmentUploads: true, fileAttachments: { maxUploadBytes: 1024 } },
},
},
states: {},
};
// Not started yet and mid-transfer both let the send through as a queued
// message; the outbox drain finishes (or redoes) the upload.
expect(composerAttachmentUploadBlockReason(input)).toBeNull();
expect(composerAttachmentsStillUploading(input)).toBe(true);
expect(
composerAttachmentsStillUploading({
...input,
states: { [key]: { status: "uploading", progress: 0.6 } },
}),
).toBe(true);
expect(composerAttachmentUploadBlockReason({ ...input, connected: false })).toBeNull();
expect(
composerAttachmentUploadBlockReason({
...input,
states: { [key]: { status: "failed", reason: "Offline" } },
}),
).toBe("Retry or remove the failed attachment");
expect(
composerAttachmentsStillUploading({
...input,
states: { [key]: { status: "failed", reason: "Offline" } },
}),
).toBe(false);
expect(
composerAttachmentUploadBlockReason({ ...input, states: { [key]: { status: "ready" } } }),
).toBeNull();
expect(
composerAttachmentsStillUploading({ ...input, states: { [key]: { status: "ready" } } }),
).toBe(false);
// Attachments the environment cannot accept never count as uploading.
expect(composerAttachmentsStillUploading({ ...input, serverConfig: null })).toBe(false);
});
});