Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
509895a
Respect recording Trash and comment settings
3mdistal Oct 22, 2026
b78d1fd
Apply recording policies to comment reads
3mdistal Oct 22, 2026
e845de2
Apply Trash access to comment cleanup
3mdistal Oct 22, 2026
5d8350c
Apply comment visibility settings to recording history
3mdistal Oct 23, 2026
d54cdfe
Let Content document readers play and comment on Clips uploaded to th…
3mdistal Oct 23, 2026
174a4c5
Expire Content playback grants in five minutes and close review gaps
3mdistal Oct 23, 2026
4890d87
Test Content Clips workspace resolution, token time fields and lifeti…
3mdistal Oct 23, 2026
954b784
Reset core's workspace caches between Content Clips tests
3mdistal Oct 23, 2026
c59ca97
Give each Content Clips workspace test its own reader
3mdistal Oct 23, 2026
22eaf54
Accept only the Content-Clips peer secret and make signed-out host ac…
3mdistal Oct 23, 2026
6e7b907
Play Clips inside Content pages behind content.clip-embed
3mdistal Oct 23, 2026
3aeed0d
Hide Clip blocks from the slash menu, add their parity row, and type …
3mdistal Oct 23, 2026
a92d094
Evaluate the Clip flag for a signed-out reader in the page's org
3mdistal Oct 23, 2026
acd8d15
Let a Clip's loading and error card fill the player's width
3mdistal Oct 23, 2026
6bea869
Keep the text toolbar off a selected registry block
3mdistal Oct 23, 2026
dfd1180
Hold a registry block's place on a public page before its editor moun…
3mdistal Oct 23, 2026
0225626
Check every pasted non-Clips link in the browser, and keep a Clip's r…
3mdistal Oct 24, 2026
b9acde2
Read at most 500 comment threads from Clips, say when there are more,…
3mdistal Oct 24, 2026
2b1f055
Name each block on a public page before it loads, keep a Clip's place…
3mdistal Oct 24, 2026
e65f245
Keep a Clip's grant to its page, cap what Content reads from Clips, c…
3mdistal Oct 24, 2026
5e2d1fc
Limit made-up Clip ids per reader and page, and cut Clip titles
3mdistal Oct 24, 2026
8daec86
Count Clip misses per Clip, so one reader can't block another's first…
3mdistal Oct 24, 2026
bee5e6d
Make a Content page's upload a private Clip bound to the page, take i…
3mdistal Oct 24, 2026
c7267b2
Tell a Content upload start that is too large or not JSON apart from …
3mdistal Oct 24, 2026
3c0bf3c
Answer a preflight to the Content chunk route for the paired Content …
3mdistal Oct 24, 2026
d307746
Let a Content page cancel an upload it gave up, and list an uploaded …
3mdistal Oct 24, 2026
6554fcb
Keep a playing Clip going when Clips misses a refresh, and bring it b…
3mdistal Oct 24, 2026
77d6911
Merge remote-tracking branch 'origin/clips-content-clip-block' into c…
3mdistal Oct 24, 2026
dad59d2
Tell a Content page whether its cancel stopped the upload or Clips fi…
3mdistal Oct 24, 2026
528c120
Answer a start Content repeats for one picked file with the upload it…
3mdistal Oct 24, 2026
66e6440
Keep every ticket a page's overlapping repeats hand out working, refu…
3mdistal Oct 24, 2026
71fb7a9
Show unavailable state for missing shared recordings
3mdistal Oct 25, 2026
bda280f
Merge recording availability update into comment visibility
3mdistal Oct 25, 2026
04d6cf0
Merge remote-tracking branch 'origin/t3/clips-comment-visibility' int…
3mdistal Oct 25, 2026
4334af5
Merge remote-tracking branch 'origin/clips-content-bindings' into cli…
3mdistal Oct 25, 2026
4669b0d
Keep unavailable recordings out of share metadata
3mdistal Oct 25, 2026
11b00e1
Merge share availability updates
3mdistal Oct 25, 2026
49c90f7
Merge remote-tracking branch 'origin/t3/clips-comment-visibility' int…
3mdistal Oct 25, 2026
cf8b022
Explain replacement of share retry assertions
3mdistal Oct 25, 2026
277d7a3
Merge share assertion documentation
3mdistal Oct 25, 2026
816f595
Merge remote-tracking branch 'origin/t3/clips-comment-visibility' int…
3mdistal Oct 25, 2026
32af525
Merge remote-tracking branch 'origin/clips-content-bindings' into cli…
3mdistal Oct 25, 2026
26eae9a
Merge remote-tracking branch 'origin/clips-content-clip-block' into c…
3mdistal Oct 25, 2026
fb8ecb6
Respect recording lifecycle in bug report context
3mdistal Oct 25, 2026
d6d3d5d
Merge bug report context lifecycle checks
3mdistal Oct 25, 2026
0fa3338
A full Clip rate-limit map drops its oldest bucket instead of refusin…
3mdistal Oct 25, 2026
a937177
Keep recording reactions independent of comments
3mdistal Oct 25, 2026
d23a26f
Merge remote-tracking branch 'origin/clips-content-clip-block' into c…
3mdistal Oct 25, 2026
b7565d8
Merge remote-tracking branch 'origin/t3/clips-comment-visibility' int…
3mdistal Oct 25, 2026
021d52b
Merge remote-tracking branch 'origin/clips-content-bindings' into cli…
3mdistal Oct 25, 2026
fef1ae8
Merge remote-tracking branch 'origin/clips-content-clip-block' into c…
3mdistal Oct 25, 2026
11a4449
Read where a Content upload stands in one place, so a cancel never ch…
3mdistal Oct 25, 2026
b274c79
Drop an unused import from the Content upload tests
3mdistal Oct 26, 2026
4c6c367
Answer a final chunk sent again with where the upload stands, never t…
3mdistal Oct 26, 2026
f2e6784
Say in the comments that a failed start is taken over only when it le…
3mdistal Oct 26, 2026
658a8f1
Renew a Content upload's lease before its final chunk claims it, so t…
3mdistal Oct 26, 2026
93bd875
Merge main and preserve Clips comment access checks
3mdistal Oct 26, 2026
1094282
Merge updated Clips access branch into comment visibility
3mdistal Oct 26, 2026
e781dc1
Apply recording Trash policy to activity recipients
3mdistal Oct 27, 2026
687fbd0
Bring activity recipient policy into comment visibility
3mdistal Oct 27, 2026
06b2271
Hide a Content upload whose page is in Trash from folders too, and as…
3mdistal Oct 27, 2026
ed81122
Leave a Content upload whose page is in Trash out of every Clips list…
3mdistal Oct 27, 2026
93384ff
Refuse a Content upload's ticket on the agent chunk route, so its chu…
3mdistal Oct 27, 2026
60c0241
Narrow recording context rows before lifecycle checks
3mdistal Oct 27, 2026
75288b2
Merge branch 't3/clips-trash-comment-install-fixes' into t3/clips-com…
3mdistal Oct 27, 2026
603d862
Preserve comment cleanup and recording search availability
3mdistal Oct 27, 2026
a089bd1
Read optional recording rows consistently across compiler checks
3mdistal Oct 27, 2026
1cde6b6
Merge branch 't3/clips-trash-comment-install-fixes' into t3/clips-com…
3mdistal Oct 27, 2026
9aafd80
Format the recording search changelog entry
3mdistal Oct 27, 2026
c2e9df8
fix(clips): clean up a failed start's binding only as that start left it
3mdistal Oct 27, 2026
72f565a
Align recording expiry checks across shared access
3mdistal Oct 27, 2026
08334b2
Match expiry timestamp precision across recording readers
3mdistal Oct 27, 2026
91d0ef6
fix(clips): refuse a page's upload on every chunk route but Content's…
3mdistal Oct 27, 2026
ad49705
Share expiry access rules with agent reads and Slack unfurls
3mdistal Oct 27, 2026
54f2da3
fix(clips): drop a final chunk's outcome when a resumed upload takes …
3mdistal Oct 27, 2026
9219a38
Apply recording expiry across Clips readers
3mdistal Oct 27, 2026
e4d86fe
Check recording expiry when content completes
3mdistal Oct 27, 2026
9564be1
Enforce recording expiry at mutation boundaries
3mdistal Oct 27, 2026
26725c3
Revalidate recording expiry before delayed writes
3mdistal Oct 27, 2026
c2ff6bb
Preserve expiry checks through queued and locked writes
3mdistal Oct 27, 2026
80a7a02
Honor linked recording permissions in meeting reads
3mdistal Oct 27, 2026
2932829
Remove unused meeting visibility test import
3mdistal Oct 27, 2026
6925321
Merge main after recording Trash landing
3mdistal Oct 27, 2026
42202d5
Recheck meeting editor access after finalization work
3mdistal Oct 27, 2026
850ae22
Merge #8088's final head (42202d5) into clips-content-bindings
3mdistal Oct 27, 2026
487b39f
Merge remote-tracking branch 'origin/main' into clips-content-bindings
3mdistal Oct 27, 2026
02cf73e
Merge commit '487b39f' into clips-content-clip-block
3mdistal Oct 27, 2026
7acd128
Meet main's strict-lint and compiler-strictness guards on Content Cli…
3mdistal Oct 27, 2026
0dc2b7e
Merge remote-tracking branch 'origin/main' into clips-content-bindings
3mdistal Oct 27, 2026
ff32560
Add #8129's attachment rule to the delegated-prompt snapshot #8140 la…
3mdistal Oct 27, 2026
1b86c6d
Merge commit 'ff32560' into clips-content-clip-block
3mdistal Oct 27, 2026
69e997e
Name the prompt behavior the snapshot update records
3mdistal Oct 27, 2026
cf5ed5f
Answer a blocked media URL under a Content grant with 502, not the ex…
3mdistal Oct 27, 2026
3ac064a
Meet main's strict-lint and compiler-strictness guards on the Clip bl…
3mdistal Oct 27, 2026
0a5bb92
Answer a storage provider's 403 under a Content grant with 502
3mdistal Oct 27, 2026
d4cb408
Merge remote-tracking branch 'origin/main' into clips-content-bindings
3mdistal Oct 27, 2026
f42d861
Merge #8090's head d4cb408 into clips-content-clip-block
3mdistal Oct 27, 2026
ea2823b
Send a signed-out page reader's Clip comments without commenter emails
3mdistal Oct 27, 2026
b090b44
Let a comment author delete through a document whatever case their em…
3mdistal Oct 27, 2026
ff1775a
Declare what the three Clip actions in Content return
3mdistal Oct 27, 2026
ab0a8e0
Set the mixed-case author without a non-null assertion
3mdistal Oct 27, 2026
ddd837e
Count a Clip request against its limits from the check, so a burst ca…
3mdistal Oct 27, 2026
c8bc0d8
Merge commit '1a390246049141e7abd671a7381480edf1689c74' into clips-co…
3mdistal Oct 27, 2026
3f31195
Merge commit 'c8bc0d8f45e4' into clips-content-upload-clips
3mdistal Oct 27, 2026
0a3d67f
Number the upload binding migrations 96 to 98, after main's 94 and 95
3mdistal Oct 27, 2026
f8aa36a
Merge commit 'f83c054c96ae0c2261231204d76454bf55315481' into clips-co…
3mdistal Oct 27, 2026
5764cbc
Clear Clips strict-lint and strict-compiler findings on the Content u…
3mdistal Oct 27, 2026
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
5 changes: 5 additions & 0 deletions templates/clips/.agents/skills/video-sharing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ A Clip placed in a Content document has one `recording_content_bindings` row per
- The token is signed with `CONTENT_CLIPS_PEER_SECRET` (`clips.contentPeerSecret`, read with `resolveContentClipsPeerSecret`), a secret only the Content and Clips deployments hold, and Clips verifies it with that secret alone. That is what makes the token's issuer trustworthy as the Content app: a token signed with the deployment `A2A_SECRET` or a workspace secret is refused, because other apps hold those. While the secret is unset, shorter than 32 characters, or equal to `A2A_SECRET`, every route answers 503 with code `content_clips_not_configured`; set it on both deployments.
- Playback returns Clips media URLs with a five-minute `content_grant` that names the reader, not a role. The grant is signed with a key derived from `CONTENT_CLIPS_PEER_SECRET`, never the app's own server secret (which a workspace derives from `A2A_SECRET`), and a media request carrying a grant while the secret is unusable gets the same 503. `/api/video` and `/api/thumbnail` re-run every gate on each request, so trashing the binding stops a URL already handed out. An expired grant is a 403 with code `content_grant_expired`; Content's player then asks Content for playback again, which re-checks document access.
- Each reader's reads and writes of one Clip through one document are rate-limited per process. A request counts against the Clip's bucket only once Clips has opened it: Content mints a token for any recording id a page reader names, so counting earlier would let a reader name made-up ids until their own spent bucket dropped out. A full map drops its oldest bucket rather than refuse a new one. A request whose Clip doesn't open counts as a miss against that reader's misses for that Clip and document; once they are spent, the Clip is refused before any database read. Misses are kept per Clip so made-up ids can't use up a real Clip's, which leaves a caller who keeps naming new made-up ids unbounded here: each costs one lookup, and Content's own per-visitor limit has to bound it. Every signed-out reader of a document shares the public reader's buckets.
- A Content editor's upload uses the `content-clip:upload` scope, whose token carries `document_id`, the editor's `name`, and the browser's `client_ip` when Content knows it, never a `recording_id`. Content checks that the reader can edit the page before minting it. `GET /api/content-clips/upload-status` answers the editor's `storage` state (`ready`, `free` with what's left, `used_up`, `none`, or `account_required` when the email has no Clips account or workspace; Clips never makes either). `POST /api/content-clips/uploads` adds the `upload` binding first, then a private recording the editor owns, and returns a ticketed `chunkUrl` and `cancelUrl`; a state that can't take the file is a 409 `storage_required` with that `storage`. The start carries the `uploadKey` Content makes once per picked file, kept on the binding with the file's type and size: a start repeated with the same key, page, and editor never makes a second Clip. It answers that upload with another ticket and the first start's plan while it is uploading, a 409 `upload_already_finished` with `recordingId` and `status` once a final chunk holds it or Clips is making the Clip, a 409 `upload_starting` while the latest start is still making its recording (Content repeats it after a few seconds), or a 409 `upload_key_used` when it was cancelled or failed, is parked until storage is connected, began in another block, or describes a file of another type or size. A start that fails deletes its binding only when nothing can be left: a refusal raised before any recording or free clip (`refusedStart`), or an error with `cleanupConfirmed: true`. Any other failure keeps the binding and marks `upload_start_failed_at`, and a repeat that finds no recording under a marked binding, or one whose latest attempt is `RESERVATION_GRACE_MS` old, takes the start over with a compare-and-swap on the binding (`takeOverStart`) and makes the recording under the binding's own recording id, so the free clip reserved per recording id is never held twice; of repeats that overlap, the rest get `upload_starting`. A kept binding whose start left a recording answers from where that upload stands, so one whose recording failed answers `upload_key_used` like a cancelled one; that key's upload ended, and Content starts the file again under a new key. Earlier tickets keep working, since overlapping repeats each hand one back: a recording keeps its four newest (`addRecordingUploadTicket`) and still holds one upload slot, while an agent's `create-recording-upload` keeps one. A repeat skips the storage check, since its first start may have taken the last free clip.
- The editor's browser posts the file to `/api/uploads/:recordingId/content-chunk` with the ticket in the query, no cookie, and no custom headers, so the request is a CORS simple request and no preflight reaches the auth guard. Only the paired built-in Content's origin (`pairedContentOrigin`) gets `Access-Control-Allow-Origin`, never the request's own `Origin`, and the answer never allows credentials.
- When the page gives an upload up (its block is gone, the editor can no longer edit, or a chunk failed for good), the browser POSTs `cancelUrl` (`/api/uploads/:recordingId/content-cancel?ticket=…`) the same way. Clips' own abort fails the recording and releases its free clip, as the lease reaper would an hour later; the `upload` binding stays. Where an upload stands is one answer, `readUploadPhase` in `server/lib/final-chunk-claim.ts`: `uploading`, `finishing` (a final chunk holds it), `parked` (Clips has the whole file and waits for storage), `finished` (`processing` or `ready`) or `ended` (cancelled or failed). The repeated start, the cancel and the Content chunk route all use it, and nothing else reads its three stores (the final chunk's claim, the upload state and the recording row) together. It reads the claim first, then the upload state, then the row. Before Clips takes the final chunk's bytes, on the buffered and the resumable path alike, the Content chunk route renews the row's lease to outlast its claim, only while the row is `uploading`, then claims the upload, changing its upload state with the same compare-and-swap the abort claims with; once Clips accepts the chunk it writes `parked` or `finished` into the upload state, and only then gives the claim up, so a reader that finds no claim finds where the upload went. A final chunk that fails gives the claim up with the upload still `uploading`. A claim older than `FINAL_CHUNK_CLAIM_TTL_MS` (a little over Clips' 75 s function timeout) was left by a request that was cut off, so the upload reads `uploading` again and the next final chunk or cancel takes it over. A live claim is never taken over: a cancel or another send of the final chunk changes nothing while the upload is `finishing` and answers 409 `upload_finishing` (`CONTENT_CLIP_CHUNK_FINISHING` for the chunk), which Content repeats every few seconds, the chunk for a little longer than the claim lasts. A final chunk sent once the upload is `parked` or `finished` answers the first one's success (202 `waitingForStorage`, or 200 with `status`) and Clips takes no bytes; one that finds the upload `ended` is refused (409). The answer's `outcome` comes from the phase read after the abort, never from the abort's `ok`: `cancelled` (200) once it is `ended`, a repeat included and a parked upload too, or `already_finished` (409, with `status`) once it is `finished`. Any other answer means it wasn't cancelled; an upload whose cancels keep finding `finishing` is left to the reaper. The reaper reads only the row's lease, never the phase: it fails a row whose lease ran out in one conditional update, so it either failed the row before the final chunk renewed it, and the chunk finds it `ended`, or it leaves the row. A page in Trash can still cancel. A closed tab is still left to the reaper.
- Upload-origin Clips are listed under From Content (`list-recordings` view `from-content`, each with `contentPageUrl` back to its page), and Library's own list leaves them out. A folder someone moved one into still lists it, and linked Clips stay in Library.
- `isActiveUploadBinding` / `activeUploadBinding()` in `server/lib/content-uploads.ts` is the one test for an active upload binding. Playback's grant, the chunk route, From Content and its page links all use it, so an upload whose page is in Trash plays for no document reader, takes no chunks (409), and lists in neither From Content nor Library.
- Create and change binding rows only through Content's upload and lifecycle flows.

## Embed URLs
Expand Down
1 change: 1 addition & 0 deletions templates/clips/actions/create-recording-upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ describe("create-recording-upload", () => {
expect(mocks.releaseRecordingUploadTicket).toHaveBeenCalledWith(
"owner@example.com",
"rec-1",
"hash",
);
});

Expand Down
89 changes: 20 additions & 69 deletions templates/clips/actions/create-recording-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,11 @@ import {
getAppProductionUrl,
getRequestContext,
} from "@agent-native/core/server";
import { UPLOAD_SLICE_BYTES } from "@shared/recording-core.js";
import { formatUploadBytes, MAX_UPLOAD_BYTES } from "@shared/upload-limits.js";
import { z } from "zod";

import { getServerAppBasePath } from "../server/lib/public-agent-context.js";
import {
createRecordingUploadTicket,
releaseRecordingUploadTicket,
} from "../server/lib/recording-upload-ticket.js";
import { getCurrentOwnerEmail, nanoid } from "../server/lib/recordings.js";
import createRecording from "./create-recording.js";
import { startRecordingUpload } from "../server/lib/recording-upload-start.js";

const MAX_UPLOAD_SIZE = formatUploadBytes(MAX_UPLOAD_BYTES, "down");

Expand Down Expand Up @@ -78,78 +72,35 @@ export default defineAction({
.describe("Video height in pixels."),
}),
run: async (args, actionContext) => {
const ownerEmail = getCurrentOwnerEmail();
// The ticket comes first: if it is refused or fails to save, no recording
// is left stranded in "uploading" with nothing able to finish it.
const recordingId = nanoid();
const ticket = await createRecordingUploadTicket(
ownerEmail,
recordingId,
getRequestContext()?.authUserId,
const upload = await startRecordingUpload(
{
title: args.title,
visibility: args.visibility,
mimeType: args.mimeType,
sizeBytes: args.sizeBytes,
durationMs: args.durationMs,
width: args.width,
height: args.height,
recordingSource: "agent_upload",
},
actionContext,
);
let created: Awaited<ReturnType<typeof createRecording.run>>;
try {
created = await createRecording.run(
{
id: recordingId,
title: args.title,
visibility: args.visibility,
mimeType: args.mimeType,
sizeBytes: args.sizeBytes,
width: args.width,
height: args.height,
hasAudio: true,
hasCamera: false,
recordingSource: "agent_upload",
requestStreaming: true,
},
actionContext,
);
} catch (error) {
// create-recording fails its own half-made row, or the lease reaper does;
// only the ticket, which the agent never received, is ours to release.
await releaseRecordingUploadTicket(ownerEmail, recordingId).catch(
(releaseError) => {
console.warn("[create-recording-upload] ticket release failed", {
recordingId,
error:
releaseError instanceof Error
? releaseError.message
: String(releaseError),
});
},
);
throw error;
}

const { recordingId, totalChunks } = upload;
const appUrl = `${new URL(getRequestContext()?.requestOrigin || getAppProductionUrl()).origin}${getServerAppBasePath()}`;
const totalChunks = Math.max(
1,
Math.ceil(args.sizeBytes / UPLOAD_SLICE_BYTES),
);
const query = new URLSearchParams({
mimeType: args.mimeType,
total: String(totalChunks),
hasAudio: "1",
hasCamera: "0",
});
if (args.durationMs) query.set("durationMs", String(args.durationMs));
if (args.width) query.set("width", String(args.width));
if (args.height) query.set("height", String(args.height));

return {
recordingId: created.id,
recordingId,
visibility: args.visibility,
shareUrl: `${appUrl}/share/${encodeURIComponent(created.id)}`,
shareUrl: `${appUrl}/share/${encodeURIComponent(recordingId)}`,
method: "POST" as const,
chunkUrl: `${appUrl}/api/uploads/${encodeURIComponent(created.id)}/agent-chunk?${query.toString()}`,
chunkUrl: `${appUrl}/api/uploads/${encodeURIComponent(recordingId)}/agent-chunk?${upload.chunkQuery.toString()}`,
headers: {
Authorization: `Bearer ${ticket.token}`,
Authorization: `Bearer ${upload.ticket}`,
"Content-Type": args.mimeType,
},
chunkSizeBytes: UPLOAD_SLICE_BYTES,
chunkSizeBytes: upload.chunkSizeBytes,
totalChunks,
expiresAt: new Date(ticket.expiresAt).toISOString(),
expiresAt: upload.expiresAt,
next: `Split the file into ${totalChunks} chunk(s) of chunkSizeBytes (the last may be shorter). POST them one at a time, in order, as the raw request body to chunkUrl + "&index=N&isFinal=0" with the returned headers, N counting from 0. Send the last chunk with "&isFinal=1": that request finalizes the recording, so do not call finalize-recording. Retry a failed chunk with the same index. The upload URL stops working once the last chunk is accepted (a retried last chunk then gets 409 saying so; read the clip with get-recording-player-data rather than uploading again) or at expiresAt. Then give the user shareUrl; transcription, and a title when none was given, follow in the background.`,
};
},
Expand Down
14 changes: 13 additions & 1 deletion templates/clips/actions/list-organization-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ vi.mock("../server/lib/agent-recording-access.js", () => ({
isAgentRecordingCaller: () => false,
}));

vi.mock("../server/lib/content-uploads.js", () => ({
uploadFromTrashedPage: () => ({ op: "upload-from-trashed-page" }),
}));

vi.mock("../server/lib/recordings.js", () => ({
getActiveOrganizationId: (...args: unknown[]) =>
mockGetActiveOrganizationId(...args),
Expand Down Expand Up @@ -62,6 +66,7 @@ vi.mock("drizzle-orm", () => ({
eq: (...args: unknown[]) => ({ op: "eq", args }),
isNotNull: (...args: unknown[]) => ({ op: "isNotNull", args }),
isNull: (...args: unknown[]) => ({ op: "isNull", args }),
not: (...args: unknown[]) => ({ op: "not", args }),
notExists: (...args: unknown[]) => ({ op: "notExists", args }),
or: (...args: unknown[]) => ({ op: "or", args }),
sql: () => ({ raw: "sql" }),
Expand All @@ -85,7 +90,7 @@ function builder(
"groupBy",
"limit",
]) {
b[method] = () => b;
b[method] = vi.fn(() => b);
}
b.then = (
resolve: (value: unknown) => unknown,
Expand Down Expand Up @@ -224,5 +229,12 @@ describe("list-organization-state action", () => {
expect(result.folders).toEqual([
expect.objectContaining({ id: "f1", recordingCount: 2 }),
]);
expect(builders[5].where).toHaveBeenCalledWith(
expect.objectContaining({
args: expect.arrayContaining([
{ op: "not", args: [{ op: "upload-from-trashed-page" }] },
]),
}),
);
});
});
3 changes: 3 additions & 0 deletions templates/clips/actions/list-organization-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
eq,
isNotNull,
isNull,
not,
notExists,
or,
sql,
Expand All @@ -24,6 +25,7 @@ import {
agentRecordingAccessFilter,
isAgentRecordingCaller,
} from "../server/lib/agent-recording-access.js";
import { uploadFromTrashedPage } from "../server/lib/content-uploads.js";
import { recordingExpiryQuery } from "../server/lib/recording-expiry-query.js";
import {
getActiveOrganizationId,
Expand Down Expand Up @@ -178,6 +180,7 @@ export default defineAction({
recordingExpiryQuery(ownerEmail).available,
isNull(schema.recordings.archivedAt),
isNull(schema.recordings.trashedAt),
not(uploadFromTrashedPage()),
notExists(
db
.select({ one: sql`1` })
Expand Down
1 change: 1 addition & 0 deletions templates/clips/actions/list-recordings.expiry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ describe("list recordings expiry with PostgreSQL", () => {
schema.recordingComments,
schema.recordingReactions,
schema.recordingEvents,
schema.recordingContentBindings,
schema.meetings,
schema.meetingShares,
schema.meetingParticipants,
Expand Down
4 changes: 4 additions & 0 deletions templates/clips/actions/list-recordings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ vi.mock("../server/db/index.js", () => ({
trashedAt: "recordings.trashedAt",
}),
recordingShares: "recordingShares",
recordingContentBindings: {
recordingId: "recordingContentBindings.recordingId",
origin: "recordingContentBindings.origin",
},
recordingTags: mockTables.recordingTags,
recordingViews: Object.assign(mockTables.recordingViews, {
recordingId: "recordingViews.recordingId",
Expand Down
Loading
Loading