mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: harden Docker image and async job responses
Harden Docker runtime packaging, preserve async job response semantics, fix Redis subscriber startup connections, clear lint warnings, and harden enterprise S3 object body handling.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAsyncAcceptedPayload } from "../../../apps/api/src/routes/async-response.js";
|
||||
|
||||
describe("buildAsyncAcceptedPayload", () => {
|
||||
it("keeps legacy async shape when the progress and artifact IDs match", () => {
|
||||
expect(buildAsyncAcceptedPayload("job-123")).toEqual({
|
||||
jobId: "job-123",
|
||||
async: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("exposes both progress and artifact IDs when a client progress ID is supplied", () => {
|
||||
expect(buildAsyncAcceptedPayload("artifact-123", "progress-456")).toEqual({
|
||||
jobId: "progress-456",
|
||||
progressJobId: "progress-456",
|
||||
artifactJobId: "artifact-123",
|
||||
async: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { RedisMock } = vi.hoisted(() => ({
|
||||
RedisMock: vi.fn(function RedisMock() {
|
||||
return {};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("ioredis", () => ({
|
||||
default: RedisMock,
|
||||
}));
|
||||
|
||||
describe("Redis connection factory", () => {
|
||||
beforeEach(() => {
|
||||
RedisMock.mockClear();
|
||||
});
|
||||
|
||||
it("keeps ready checks enabled for command connections", async () => {
|
||||
const { createRedisConnection } = await import("../../../../apps/api/src/jobs/connection.js");
|
||||
|
||||
createRedisConnection();
|
||||
|
||||
expect(RedisMock).toHaveBeenCalledWith(expect.any(String), {
|
||||
enableReadyCheck: true,
|
||||
maxRetriesPerRequest: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("disables ready checks for pub/sub-only subscriber connections", async () => {
|
||||
const { createRedisSubscriberConnection } = await import(
|
||||
"../../../../apps/api/src/jobs/connection.js"
|
||||
);
|
||||
|
||||
createRedisSubscriberConnection();
|
||||
|
||||
expect(RedisMock).toHaveBeenCalledWith(expect.any(String), {
|
||||
enableReadyCheck: false,
|
||||
maxRetriesPerRequest: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -216,6 +216,7 @@ function createMockRequest(opts: {
|
||||
filename?: string;
|
||||
settings?: string;
|
||||
fileId?: string;
|
||||
clientJobId?: string;
|
||||
fileCount?: number;
|
||||
}) {
|
||||
const parts: Array<{
|
||||
@@ -255,6 +256,15 @@ function createMockRequest(opts: {
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.clientJobId) {
|
||||
parts.push({
|
||||
type: "field",
|
||||
fieldname: "clientJobId",
|
||||
value: opts.clientJobId,
|
||||
file: (async function* () {})(),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
parts: () => ({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
@@ -271,6 +281,8 @@ function createMockRequest(opts: {
|
||||
describe("createToolRoute", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(isToolInstalled).mockReset();
|
||||
vi.mocked(isToolInstalled).mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe("route registration", () => {
|
||||
@@ -505,6 +517,61 @@ describe("createToolRoute", () => {
|
||||
expect(reply.send).toHaveBeenCalledWith(expect.objectContaining({ async: true }));
|
||||
});
|
||||
|
||||
it("returns the artifact job ID when a client progress job ID is supplied", async () => {
|
||||
vi.mocked(waitForJob).mockResolvedValueOnce(null);
|
||||
const app = createMockApp();
|
||||
const id = "resize";
|
||||
createToolRoute(app as never, makeMockConfig(id));
|
||||
const handler = app.routes[apiToolPath(id)];
|
||||
const reply = createMockReply();
|
||||
const clientJobId = "client-progress-id";
|
||||
const req = createMockRequest({
|
||||
fileBuffer: Buffer.from("png-data"),
|
||||
settings: JSON.stringify({}),
|
||||
clientJobId,
|
||||
});
|
||||
|
||||
await handler(req, reply);
|
||||
|
||||
const artifactJobId = vi.mocked(enqueueToolJob).mock.calls[0][0].jobId;
|
||||
expect(artifactJobId).not.toBe(clientJobId);
|
||||
expect(reply.status).toHaveBeenCalledWith(202);
|
||||
expect(reply.send).toHaveBeenCalledWith({
|
||||
jobId: clientJobId,
|
||||
progressJobId: clientJobId,
|
||||
artifactJobId,
|
||||
async: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the artifact job ID immediately for long tools with client progress IDs", async () => {
|
||||
vi.mocked(isToolInstalled).mockReturnValue(true);
|
||||
const app = createMockApp();
|
||||
const id = "upscale";
|
||||
createToolRoute(app as never, makeMockConfig(id));
|
||||
const handler = app.routes[apiToolPath(id)];
|
||||
const reply = createMockReply();
|
||||
const clientJobId = "client-long-progress-id";
|
||||
const req = createMockRequest({
|
||||
fileBuffer: Buffer.from("png-data"),
|
||||
settings: JSON.stringify({}),
|
||||
clientJobId,
|
||||
});
|
||||
|
||||
await handler(req, reply);
|
||||
|
||||
const artifactJobId = vi.mocked(enqueueToolJob).mock.calls[0][0].jobId;
|
||||
expect(waitForJob).not.toHaveBeenCalled();
|
||||
expect(artifactJobId).not.toBe(clientJobId);
|
||||
expect(reply.status).toHaveBeenCalledWith(202);
|
||||
expect(reply.send).toHaveBeenCalledWith({
|
||||
jobId: clientJobId,
|
||||
progressJobId: clientJobId,
|
||||
artifactJobId,
|
||||
async: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 422 when waitForJob rejects", async () => {
|
||||
vi.mocked(waitForJob).mockRejectedValueOnce(new Error("Sharp exploded"));
|
||||
const app = createMockApp();
|
||||
|
||||
Reference in New Issue
Block a user