Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/desktop-gpui/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 1 addition & 5 deletions apps/desktop-gpui/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ const ICONS: &[(&str, &[u8])] = assets!("icons":
"laptop.svg",
"shuffle.svg",
"gift.svg",
"history.svg",
"hotkeys.svg",
"image.svg",
"info.svg",
Expand All @@ -99,7 +98,6 @@ const ICONS: &[(&str, &[u8])] = assets!("icons":
"message-square-plus.svg",
"mic-off.svg",
"microphone.svg",
"minimize.svg",
"minus.svg",
"monitor.svg",
"more-vertical.svg",
Expand Down Expand Up @@ -186,10 +184,7 @@ const ICONS: &[(&str, &[u8])] = assets!("icons":
"copy.svg",
"record-fill.svg",
"warning-bold.svg",
// The main window's hand-drawn traffic lights: the x and expand glyphs
// `CaptionControlsMacOS.tsx` inlines, shown while the group is hovered.
"traffic-close.svg",
"traffic-zoom.svg",
// The remaining settings pages (`settings_pages.rs`). `circle-check` is
// Cap's own (`packages/ui-solid/icons/circle-check.svg`, hotkeys.tsx's
// IconCapCircleCheck); the rest are the Lucide 24x24 originals the pages'
Expand Down Expand Up @@ -335,6 +330,7 @@ mod tests {
// `ui::SelectionHeader` names the check and the trash itself.
include_str!("ui/selection_header.rs"),
include_str!("ui/radio_cards.rs"),
include_str!("ui/windows_caption.rs"),
// The onboarding window's welcome cards and permissions surface; the
// per-permission row glyphs are named on `OSPermission::icon`.
include_str!("onboarding_window.rs"),
Expand Down
135 changes: 129 additions & 6 deletions apps/web/__tests__/unit/desktop-video-create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const { Storage } = await import("@cap/web-backend");
const { invalidateGoogleDriveStorageQuotaCache } = await import(
"@/lib/google-drive-storage-quota"
);
const { maybeStartLiveTranscription } = await import("@/lib/live-transcribe");

function resetMockDb() {
for (const key of Object.keys(mockDb)) {
Expand Down Expand Up @@ -170,6 +171,50 @@ function insertedValues(table: unknown) {
| undefined;
}

function recordingTransactionFixture(rejectUpload: boolean) {
type Row = Record<string, unknown>;
type Rows = { videos: Map<string, Row>; uploads: Map<string, Row> };
const committed: Rows = { videos: new Map(), uploads: new Map() };
const transactionInserts: unknown[] = [];
const directInserts: unknown[] = [];
const insertInto = (rows: Rows, inserts: unknown[]) => (table: unknown) => {
inserts.push(table);
return {
values: async (value: Row) => {
if (table === schema.videoUploads && rejectUpload)
throw new Error("Injected upload insert failure");
const target =
table === schema.videos
? rows.videos
: table === schema.videoUploads
? rows.uploads
: null;
if (!target) throw new Error("Unexpected insert table");
const id = String(table === schema.videos ? value.id : value.videoId);
target.set(id, { ...value });
},
};
};
mockDb.insert.mockImplementation(insertInto(committed, directInserts));
mockDb.transaction.mockImplementation(
async (
run: (tx: { insert: ReturnType<typeof insertInto> }) => Promise<unknown>,
) => {
const candidate: Rows = {
videos: new Map(committed.videos),
uploads: new Map(committed.uploads),
};
const result = await run({
insert: insertInto(candidate, transactionInserts),
});
committed.videos = candidate.videos;
committed.uploads = candidate.uploads;
return result;
},
);
return { committed, transactionInserts, directInserts };
}

function stubStorage() {
const getOrganizationWritableAccess =
Storage.getOrganizationWritableAccess as ReturnType<typeof vi.fn>;
Expand Down Expand Up @@ -420,7 +465,7 @@ describe("GET /create", () => {
const response = await app.request("https://cap.test/create");

expect(response.status).toBe(200);
expect(mockDb.transaction).toHaveBeenCalledTimes(1);
expect(mockDb.transaction).toHaveBeenCalledTimes(2);

const orgValues = insertedValues(schema.organizations) as
| { id: string; ownerId: string; name: string }
Expand Down Expand Up @@ -469,7 +514,7 @@ describe("GET /create", () => {
const response = await app.request("https://cap.test/create");

expect(response.status).toBe(200);
expect(mockDb.transaction).not.toHaveBeenCalled();
expect(mockDb.transaction).toHaveBeenCalledTimes(1);

expect(insertedValues(schema.videos)).toMatchObject({
orgId: "org-1",
Expand All @@ -494,7 +539,7 @@ describe("GET /create", () => {
const response = await app.request("https://cap.test/create");

expect(response.status).toBe(200);
expect(mockDb.transaction).toHaveBeenCalledTimes(1);
expect(mockDb.transaction).toHaveBeenCalledTimes(2);

const orgValues = insertedValues(schema.organizations) as
| { id: string }
Expand Down Expand Up @@ -524,7 +569,7 @@ describe("GET /create", () => {
);

expect(response.status).toBe(200);
expect(mockDb.transaction).toHaveBeenCalledTimes(1);
expect(mockDb.transaction).toHaveBeenCalledTimes(2);

const orgValues = insertedValues(schema.organizations) as
| { id: string }
Expand Down Expand Up @@ -558,7 +603,7 @@ describe("GET /create", () => {
);

expect(response.status).toBe(200);
expect(mockDb.transaction).not.toHaveBeenCalled();
expect(mockDb.transaction).toHaveBeenCalledTimes(1);

expect(insertedValues(schema.videos)).toMatchObject({
orgId: "org-1",
Expand All @@ -585,7 +630,7 @@ describe("GET /create", () => {
const response = await app.request("https://cap.test/create?orgId=org-2");

expect(response.status).toBe(200);
expect(mockDb.transaction).not.toHaveBeenCalled();
expect(mockDb.transaction).toHaveBeenCalledTimes(1);

expect(insertedValues(schema.videos)).toMatchObject({
orgId: "org-2",
Expand Down Expand Up @@ -620,6 +665,84 @@ describe("GET /create", () => {
expect(await response.json()).toMatchObject({ id: "0123456789abcde" });
});

it("commits a progress-capable recording and its upload row in one transaction", async () => {
mockGetCurrentUser.mockResolvedValue({
id: "fixture-user",
email: "fixture@example.com",
defaultOrgId: "fixture-org",
activeOrganizationId: "fixture-org",
});
mockDb.where
.mockResolvedValueOnce([
{
id: "fixture-org",
name: "Fixture organization",
createdAt: new Date("2026-01-01T00:00:00.000Z"),
},
])
.mockResolvedValueOnce([]);
const fixture = recordingTransactionFixture(false);

const response = await app.request(
"https://cap.test/create?recordingMode=desktopMP4",
{ headers: { "X-Cap-Desktop-Version": "0.3.68" } },
);

expect(response.status).toBe(200);
expect(mockDb.transaction).toHaveBeenCalledTimes(1);
expect(fixture.transactionInserts).toEqual([
schema.videos,
schema.videoUploads,
]);
expect(fixture.directInserts).toEqual([]);
const body = await response.json();
expect(fixture.committed.videos.get(body.id)).toMatchObject({
id: body.id,
ownerId: "fixture-user",
orgId: "fixture-org",
});
expect(fixture.committed.uploads.get(body.id)).toMatchObject({
videoId: body.id,
mode: "singlepart",
});
});

it("rolls back a recording and skips transcription when its upload insert fails", async () => {
mockGetCurrentUser.mockResolvedValue({
id: "fixture-user",
email: "fixture@example.com",
defaultOrgId: "fixture-org",
activeOrganizationId: "fixture-org",
});
mockDb.where
.mockResolvedValueOnce([
{
id: "fixture-org",
name: "Fixture organization",
createdAt: new Date("2026-01-01T00:00:00.000Z"),
},
])
.mockResolvedValueOnce([]);
const fixture = recordingTransactionFixture(true);
vi.spyOn(console, "error").mockImplementation(() => undefined);

const response = await app.request(
"https://cap.test/create?recordingMode=desktopSegments",
{ headers: { "X-Cap-Desktop-Version": "0.3.68" } },
);

expect(response.status).toBe(500);
expect(mockDb.transaction).toHaveBeenCalledTimes(1);
expect(fixture.transactionInserts).toEqual([
schema.videos,
schema.videoUploads,
]);
expect(fixture.directInserts).toEqual([]);
expect(fixture.committed.videos.size).toBe(0);
expect(fixture.committed.uploads.size).toBe(0);
expect(maybeStartLiveTranscription).not.toHaveBeenCalled();
});

it("rejects an invalid client-selected video ID", async () => {
mockGetCurrentUser.mockResolvedValue({
id: "user-1",
Expand Down
27 changes: 26 additions & 1 deletion apps/web/__tests__/unit/playback-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,37 @@ describe("resolvePlaybackSource", () => {
fetchImpl,
now: () => 123,
}),
).toEqual({ url: initialUrl, type: "mp4", supportsCrossOrigin: true });
).toEqual({
url: initialUrl,
type: "mp4",
supportsCrossOrigin: true,
fromInitialUrl: true,
});
expect(fetchImpl).toHaveBeenCalledExactlyOnceWith(initialUrl, {
headers: { range: "bytes=0-0" },
});
});

it("retains the initial URL origin after a signed media redirect", async () => {
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValueOnce(
createResponse("https://media.example.com/redirected.mp4", {
status: 206,
redirected: true,
}),
);
expect(
await resolvePlaybackSource({
videoSrc: "/api/playlist?videoType=mp4",
initialUrl: "https://media.example.com/initial.mp4",
fetchImpl,
}),
).toMatchObject({
url: "https://media.example.com/redirected.mp4",
fromInitialUrl: true,
});
expect(fetchImpl).toHaveBeenCalledTimes(1);
});

it.each([401, 403, 404, 500])(
"refreshes a failed initial URL through the authorized playlist route (HTTP %s)",
async (status) => {
Expand Down
Loading
Loading