@frockbot/plugin-authoring 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/frockbot.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "schemaVersion": 3,
3
+ "id": "authoring",
4
+ "displayName": "Package Authoring",
5
+ "version": "0.0.1",
6
+ "compatibility": {
7
+ "frockbot": ">=0.0.1"
8
+ },
9
+ "dependencies": {
10
+ "shell": ">=0.0.1"
11
+ },
12
+ "contributions": {
13
+ "runtime": {
14
+ "entry": "./agent"
15
+ }
16
+ },
17
+ "permissions": ["packages:author"]
18
+ }
package/package.json CHANGED
@@ -1,14 +1,39 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-authoring",
3
- "version": "0.0.0",
4
- "description": "Placeholder reserving this name for trusted publishing. Superseded by the first release.",
5
- "license": "UNLICENSED",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./agent": "./src/agent.ts",
9
+ "./manifest": "./src/manifest.ts",
10
+ "./quota": "./src/quota.ts",
11
+ "./records": "./src/records.ts",
12
+ "./shared": "./src/shared.ts",
13
+ "./frockbot.json": "./frockbot.json",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "frockbot": {
17
+ "manifest": "./frockbot.json"
18
+ },
19
+ "scripts": {
20
+ "test": "bun test src",
21
+ "typecheck": "tsc --noEmit -p tsconfig.json"
22
+ },
23
+ "dependencies": {
24
+ "@frockbot/kernel-contracts": "0.1.1",
25
+ "cordis": "4.0.0-rc.8"
26
+ },
27
+ "devDependencies": {
28
+ "@types/bun": "1.4.0",
29
+ "typescript": "^7.0.2"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
6
34
  "repository": {
7
35
  "type": "git",
8
36
  "url": "git+https://github.com/timoconnellaus/frockbot.git",
9
37
  "directory": "packages/plugin-authoring"
10
- },
11
- "publishConfig": {
12
- "access": "public"
13
38
  }
14
39
  }
@@ -0,0 +1,162 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { SessionStore, type Session } from "@frockbot/kernel-contracts";
3
+ import { Context } from "cordis";
4
+ import {
5
+ createPackageAuthorTool,
6
+ openTurnPositionV1,
7
+ type AuthorPackageRequestV1,
8
+ type PackageAuthoringHost,
9
+ } from "./agent.ts";
10
+ import type { AuthorPackageOutcomeV1 } from "./shared.ts";
11
+
12
+ const INPUT = {
13
+ packageId: "weather-lookup",
14
+ displayName: "Weather lookup",
15
+ tool: { name: "weather_lookup", description: "Looks up", inputSchema: {} },
16
+ source: "export const tools = [];\nexport async function execute() {}\n",
17
+ };
18
+
19
+ const CONTEXT = {
20
+ botId: "bot-1",
21
+ agentId: "bot-1",
22
+ sessionId: "user-1:bot-1",
23
+ compositionGenerationId: "2026-08-31T00:00:00.000Z:0123456789abcdef",
24
+ turnType: "chat" as const,
25
+ effectId: "tool:1:1:0",
26
+ signal: new AbortController().signal,
27
+ };
28
+
29
+ async function openSession(): Promise<{
30
+ session: Session;
31
+ sessions: { get(id: string): Session | undefined };
32
+ dispose(): Promise<void>;
33
+ }> {
34
+ const root = new Context();
35
+ await root.plugin(SessionStore);
36
+ const session = root.sessions.create("user-1:bot-1");
37
+ session.appendBatch([
38
+ { type: "turn/start", turn: 4 },
39
+ { type: "step/start", turn: 4, step: 2 },
40
+ ]);
41
+ return {
42
+ session,
43
+ sessions: root.sessions,
44
+ dispose: () => root.fiber.dispose(),
45
+ };
46
+ }
47
+
48
+ function stubHost(
49
+ outcome: AuthorPackageOutcomeV1,
50
+ seen: AuthorPackageRequestV1[] = [],
51
+ ): PackageAuthoringHost {
52
+ return {
53
+ effectIdFor: () => Promise.resolve("author-0123456789abcdef"),
54
+ author: (request) => {
55
+ seen.push(request);
56
+ return Promise.resolve(outcome);
57
+ },
58
+ };
59
+ }
60
+
61
+ describe("the package_author tool", () => {
62
+ test("records intent before the effect and the outcome after it", async () => {
63
+ const { session, sessions, dispose } = await openSession();
64
+ const seen: AuthorPackageRequestV1[] = [];
65
+ const tool = createPackageAuthorTool(
66
+ stubHost(
67
+ {
68
+ status: "authored",
69
+ packageId: "weather-lookup",
70
+ version: "0.0.1",
71
+ contentHash: "b".repeat(64),
72
+ generationId: "2026-08-31T01:00:00.000Z:fedcba9876543210",
73
+ },
74
+ seen,
75
+ ),
76
+ sessions,
77
+ );
78
+
79
+ const result = await tool.execute(INPUT, CONTEXT);
80
+
81
+ expect(result.isError).toBe(false);
82
+ expect(result.content).toContain("version 0.0.1");
83
+ expect(result.content).toContain(
84
+ "2026-08-31T01:00:00.000Z:fedcba9876543210",
85
+ );
86
+ expect(result.content).toContain("activates on the next Turn");
87
+ const types = session.events.map((event) => event.type);
88
+ expect(types.slice(-2)).toEqual([
89
+ "package/author-intent",
90
+ "package/authored",
91
+ ]);
92
+ // The intent is recorded before the host is reached at all.
93
+ expect(seen).toHaveLength(1);
94
+ expect(seen[0]?.position).toEqual({ turn: 4, step: 2 });
95
+ await dispose();
96
+ });
97
+
98
+ test("a refusal leaves the intent recorded and no authored event", async () => {
99
+ const { session, sessions, dispose } = await openSession();
100
+ const tool = createPackageAuthorTool(
101
+ stubHost({
102
+ status: "refused",
103
+ reason: "a durable per-User quota refused this Package",
104
+ failureId: "authoring-failure-1",
105
+ }),
106
+ sessions,
107
+ );
108
+
109
+ const result = await tool.execute(INPUT, CONTEXT);
110
+
111
+ expect(result.isError).toBe(true);
112
+ expect(result.content).toContain("authoring-failure-1");
113
+ expect(session.events.map((event) => event.type)).toContain(
114
+ "package/author-intent",
115
+ );
116
+ expect(session.events.map((event) => event.type)).not.toContain(
117
+ "package/authored",
118
+ );
119
+ await dispose();
120
+ });
121
+
122
+ test("an undecodable input is a tool error, not a durable effect", async () => {
123
+ const { session, sessions, dispose } = await openSession();
124
+ const seen: AuthorPackageRequestV1[] = [];
125
+ const tool = createPackageAuthorTool(
126
+ stubHost(
127
+ {
128
+ status: "authored",
129
+ packageId: "x",
130
+ version: "0.0.1",
131
+ contentHash: "b".repeat(64),
132
+ generationId: "g",
133
+ },
134
+ seen,
135
+ ),
136
+ sessions,
137
+ );
138
+
139
+ expect(tool.validate?.({ packageId: "Weather" })).toBe(false);
140
+ const result = await tool.execute({ packageId: "Weather" }, CONTEXT);
141
+
142
+ expect(result.isError).toBe(true);
143
+ expect(seen).toHaveLength(0);
144
+ expect(session.events.map((event) => event.type)).not.toContain(
145
+ "package/author-intent",
146
+ );
147
+ await dispose();
148
+ });
149
+
150
+ test("authoring outside an open step is refused", async () => {
151
+ const root = new Context();
152
+ await root.plugin(SessionStore);
153
+ const session = root.sessions.create("user-1:bot-1");
154
+ session.appendBatch([
155
+ { type: "turn/start", turn: 1 },
156
+ { type: "step/start", turn: 1, step: 1 },
157
+ { type: "step/end", turn: 1, step: 1, outcome: "completed" },
158
+ ]);
159
+ expect(() => openTurnPositionV1(session)).toThrow();
160
+ await root.fiber.dispose();
161
+ });
162
+ });
package/src/agent.ts ADDED
@@ -0,0 +1,190 @@
1
+ // The Package Authoring runtime Contribution: one tool, `package_author`.
2
+ //
3
+ // The Package holds no authority of its own. It decodes the model's input at
4
+ // the seam, appends the two session events that make the effect visible in the
5
+ // durable log, and hands the work to the authoring host the Durable Object
6
+ // gave it. It never reaches a Worker `env`, a bundler, object storage, or the
7
+ // Composition store directly.
8
+ import type {
9
+ Session,
10
+ ToolDefinition,
11
+ ToolExecutionContext,
12
+ } from "@frockbot/kernel-contracts";
13
+ import type { Plugin } from "cordis";
14
+ import {
15
+ AUTHOR_PACKAGE_INPUT_SCHEMA_V1,
16
+ type AuthorPackageInputV1,
17
+ type AuthorPackageOutcomeV1,
18
+ decodeAuthorPackageInputV1,
19
+ sha256HexV1,
20
+ } from "./shared.js";
21
+
22
+ /** The turn and step an authoring event is recorded under. */
23
+ export interface AuthoringTurnPositionV1 {
24
+ turn: number;
25
+ step: number;
26
+ }
27
+
28
+ export interface AuthorPackageRequestV1 {
29
+ input: AuthorPackageInputV1;
30
+ sourceHash: string;
31
+ effectId: string;
32
+ sessionId: string;
33
+ position: AuthoringTurnPositionV1;
34
+ }
35
+
36
+ /**
37
+ * The kernel-hosted seam this Package receives. Implemented by the Durable
38
+ * Object host (`@frockbot/plugin-shell/backend-authoring`), which owns the
39
+ * intent record, the `PACKAGE_BUNDLER` binding, the artifact store, the User
40
+ * quota RPC, and the Composition store.
41
+ */
42
+ export interface PackageAuthoringHost {
43
+ /** The idempotency key for this call; deterministic in the admitted run. */
44
+ effectIdFor(input: {
45
+ packageId: string;
46
+ sourceHash: string;
47
+ }): Promise<string>;
48
+ author(request: AuthorPackageRequestV1): Promise<AuthorPackageOutcomeV1>;
49
+ }
50
+
51
+ /**
52
+ * The open step an authoring event belongs to. The session log is the
53
+ * reconstruction surface, so an authoring event without its turn and step
54
+ * would not replay in place.
55
+ */
56
+ export function openTurnPositionV1(session: Session): AuthoringTurnPositionV1 {
57
+ const started = session.events.findLast(
58
+ (event) => event.type === "step/start",
59
+ );
60
+ const ended = session.events.findLast((event) => event.type === "step/end");
61
+ if (started?.type !== "step/start") {
62
+ throw new Error("package authoring has no open step to record against");
63
+ }
64
+ if (
65
+ ended?.type === "step/end" &&
66
+ ended.turn === started.turn &&
67
+ ended.step === started.step
68
+ ) {
69
+ throw new Error("package authoring has no open step to record against");
70
+ }
71
+ return { turn: started.turn, step: started.step };
72
+ }
73
+
74
+ function refusalText(
75
+ outcome: Extract<AuthorPackageOutcomeV1, { status: "refused" }>,
76
+ ): string {
77
+ return [
78
+ `Authoring was refused: ${outcome.reason}`,
79
+ `A durable failure record "${outcome.failureId}" was written; the User can inspect it. Nothing was activated.`,
80
+ ].join(" ");
81
+ }
82
+
83
+ function authoredText(
84
+ outcome: Extract<AuthorPackageOutcomeV1, { status: "authored" }>,
85
+ ): string {
86
+ return [
87
+ `Authored Package "${outcome.packageId}" version ${outcome.version}.`,
88
+ outcome.supersededVersion
89
+ ? `It supersedes version ${outcome.supersededVersion}.`
90
+ : undefined,
91
+ `Its artifact is ${outcome.contentHash} and it is recorded as Composition generation ${outcome.generationId}.`,
92
+ "That generation is pending: this Turn keeps running on the Composition it was admitted under, and the new Package activates on the next Turn.",
93
+ ]
94
+ .filter((part): part is string => part !== undefined)
95
+ .join(" ");
96
+ }
97
+
98
+ export function createPackageAuthorTool(
99
+ host: PackageAuthoringHost,
100
+ sessions: { get(sessionId: string): Session | undefined },
101
+ ): ToolDefinition {
102
+ return {
103
+ name: "package_author",
104
+ // A general work tool: the full toolset an `executor` subagent gets, and
105
+ // not part of the narrow reach of `browserUse`, `computerUse`, or the two
106
+ // video roles. See `@frockbot/plugin-subagents` `SUBAGENT_TOOL_REACH_V1`.
107
+ admission: { subagentRoles: ["executor"] },
108
+ description:
109
+ "Author a Package for yourself: one tool implemented in a single TypeScript file that runs in your own isolate. The Package is recorded as a new Composition generation and activates on your next Turn.",
110
+ inputSchema: AUTHOR_PACKAGE_INPUT_SCHEMA_V1,
111
+ idempotent: false,
112
+ validate: (input: unknown) => {
113
+ try {
114
+ decodeAuthorPackageInputV1(input);
115
+ return true;
116
+ } catch {
117
+ return false;
118
+ }
119
+ },
120
+ execute: async (input: unknown, context: ToolExecutionContext) => {
121
+ let decoded: AuthorPackageInputV1;
122
+ try {
123
+ decoded = decodeAuthorPackageInputV1(input);
124
+ } catch (error) {
125
+ return {
126
+ content: `package_author input was rejected: ${
127
+ error instanceof Error ? error.message : String(error)
128
+ }`,
129
+ isError: true,
130
+ };
131
+ }
132
+ const session = sessions.get(context.sessionId);
133
+ if (!session) {
134
+ return {
135
+ content: `package_author cannot record its intent: session "${context.sessionId}" is unavailable`,
136
+ isError: true,
137
+ };
138
+ }
139
+ const sourceHash = await sha256HexV1(decoded.source);
140
+ const effectId = await host.effectIdFor({
141
+ packageId: decoded.packageId,
142
+ sourceHash,
143
+ });
144
+ const position = openTurnPositionV1(session);
145
+ // Intent before effect: the session event and the durable intent record
146
+ // are both written before the bundler is reached.
147
+ session.append({
148
+ type: "package/author-intent",
149
+ ...position,
150
+ effectId,
151
+ packageId: decoded.packageId,
152
+ sourceHash,
153
+ });
154
+ await session.flush();
155
+
156
+ const outcome = await host.author({
157
+ input: decoded,
158
+ sourceHash,
159
+ effectId,
160
+ sessionId: context.sessionId,
161
+ position,
162
+ });
163
+ if (outcome.status === "refused") {
164
+ return { content: refusalText(outcome), isError: true };
165
+ }
166
+ session.append({
167
+ type: "package/authored",
168
+ ...position,
169
+ effectId,
170
+ packageId: outcome.packageId,
171
+ version: outcome.version,
172
+ contentHash: outcome.contentHash,
173
+ generationId: outcome.generationId,
174
+ });
175
+ // The model must not be told it succeeded before the record is durable.
176
+ await session.flush();
177
+ return { content: authoredText(outcome), isError: false };
178
+ },
179
+ };
180
+ }
181
+
182
+ /** The runtime Contribution. Registers `package_author` and nothing else. */
183
+ export function createAuthoringRuntimePlugin(
184
+ host: PackageAuthoringHost,
185
+ ): Plugin.Function {
186
+ const plugin: Plugin.Function = (ctx) =>
187
+ ctx.tools.register(createPackageAuthorTool(host, ctx.sessions));
188
+ plugin.inject = ["tools", "sessions"];
189
+ return plugin;
190
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./agent.js";
2
+ export { default as authoringManifest } from "./manifest.js";
3
+ export * from "./quota.js";
4
+ export * from "./records.js";
5
+ export * from "./shared.js";
@@ -0,0 +1,3 @@
1
+ import manifest from "../frockbot.json" with { type: "json" };
2
+
3
+ export default manifest;
@@ -0,0 +1,213 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ AUTHORING_QUOTA_CONFIG_KEY,
4
+ AUTHORING_QUOTA_DEFAULTS_V1,
5
+ authoringQuotaCounterKey,
6
+ authoringQuotaDayV1,
7
+ decodeAuthoringQuotaConfigV1,
8
+ decodeAuthoringQuotaReceiptV1,
9
+ reserveAuthoringQuotaV1,
10
+ type AuthoringQuotaStorage,
11
+ } from "./quota.ts";
12
+
13
+ function storage(initial: Record<string, unknown> = {}) {
14
+ const values = new Map<string, unknown>(Object.entries(initial));
15
+ // Durable Object transactions are serialized; this fake is too, so a
16
+ // read-modify-write that is *not* wrapped in one can interleave here exactly
17
+ // as it would in the object.
18
+ let queue: Promise<unknown> = Promise.resolve();
19
+ const store: AuthoringQuotaStorage & { values: Map<string, unknown> } = {
20
+ values,
21
+ get: <T>(key: string) => Promise.resolve(values.get(key) as T | undefined),
22
+ put: (key: string, value: unknown) => {
23
+ values.set(key, value);
24
+ return Promise.resolve();
25
+ },
26
+ transaction: <T>(
27
+ callback: (storage: AuthoringQuotaStorage) => Promise<T>,
28
+ ) => {
29
+ const run = queue.then(() => callback(store));
30
+ queue = run.then(
31
+ () => undefined,
32
+ () => undefined,
33
+ );
34
+ return run;
35
+ },
36
+ };
37
+ return store;
38
+ }
39
+
40
+ const REQUEST = {
41
+ schemaVersion: 1 as const,
42
+ userId: "user-1",
43
+ botId: "bot-1",
44
+ effectId: "author-0123456789abcdef",
45
+ day: "2026-08-31",
46
+ sourceBytes: 1_024,
47
+ retainedGenerations: 3,
48
+ };
49
+
50
+ describe("the durable per-User authoring quota", () => {
51
+ test("defaults to 50 retained generations, 100 a day, and 256 KB of source", () => {
52
+ expect(AUTHORING_QUOTA_DEFAULTS_V1).toEqual({
53
+ schemaVersion: 1,
54
+ retainedGenerationsPerBot: 50,
55
+ authoredPerUserPerDay: 100,
56
+ maxSourceBytes: 256 * 1024,
57
+ });
58
+ expect(decodeAuthoringQuotaConfigV1(undefined)).toEqual(
59
+ AUTHORING_QUOTA_DEFAULTS_V1,
60
+ );
61
+ });
62
+
63
+ test("reserves a unit and counts it under quota:generations:<yyyy-mm-dd>", async () => {
64
+ const store = storage();
65
+ const receipt = await reserveAuthoringQuotaV1(store, REQUEST);
66
+
67
+ expect(receipt).toMatchObject({ status: "reserved", used: 1, limit: 100 });
68
+ expect(store.values.get(authoringQuotaCounterKey("2026-08-31"))).toEqual({
69
+ day: "2026-08-31",
70
+ count: 1,
71
+ });
72
+ });
73
+
74
+ test("admits exactly one of two concurrent reservations at the limit", async () => {
75
+ const store = storage({
76
+ [AUTHORING_QUOTA_CONFIG_KEY]: {
77
+ ...AUTHORING_QUOTA_DEFAULTS_V1,
78
+ authoredPerUserPerDay: 100,
79
+ },
80
+ [authoringQuotaCounterKey("2026-08-31")]: {
81
+ day: "2026-08-31",
82
+ count: 99,
83
+ },
84
+ });
85
+
86
+ // The counter is a read-modify-write across awaits; without a transaction
87
+ // both reservations read 99 and both admit.
88
+ const [first, second] = await Promise.all([
89
+ reserveAuthoringQuotaV1(store, { ...REQUEST, effectId: "author-race-a" }),
90
+ reserveAuthoringQuotaV1(store, { ...REQUEST, effectId: "author-race-b" }),
91
+ ]);
92
+
93
+ expect(
94
+ [first.status, second.status].filter((status) => status === "reserved"),
95
+ ).toHaveLength(1);
96
+ expect(
97
+ [first, second].filter((receipt) => receipt.status === "refused"),
98
+ ).toMatchObject([{ limitName: "authored-per-day" }]);
99
+ expect(store.values.get(authoringQuotaCounterKey("2026-08-31"))).toEqual({
100
+ day: "2026-08-31",
101
+ count: 100,
102
+ });
103
+ });
104
+
105
+ test("is idempotent on the authoring effect id", async () => {
106
+ const store = storage();
107
+ const first = await reserveAuthoringQuotaV1(store, REQUEST);
108
+ const second = await reserveAuthoringQuotaV1(store, REQUEST);
109
+
110
+ expect(second).toEqual(first);
111
+ expect(store.values.get(authoringQuotaCounterKey("2026-08-31"))).toEqual({
112
+ day: "2026-08-31",
113
+ count: 1,
114
+ });
115
+ });
116
+
117
+ test("refuses rather than throws when the daily generation quota is spent", async () => {
118
+ const store = storage({
119
+ [AUTHORING_QUOTA_CONFIG_KEY]: {
120
+ ...AUTHORING_QUOTA_DEFAULTS_V1,
121
+ authoredPerUserPerDay: 2,
122
+ },
123
+ });
124
+ await reserveAuthoringQuotaV1(store, { ...REQUEST, effectId: "author-a" });
125
+ await reserveAuthoringQuotaV1(store, { ...REQUEST, effectId: "author-b" });
126
+ const refused = await reserveAuthoringQuotaV1(store, {
127
+ ...REQUEST,
128
+ effectId: "author-c",
129
+ });
130
+
131
+ expect(refused).toMatchObject({
132
+ status: "refused",
133
+ limitName: "authored-per-day",
134
+ used: 2,
135
+ limit: 2,
136
+ });
137
+ expect(store.values.get(authoringQuotaCounterKey("2026-08-31"))).toEqual({
138
+ day: "2026-08-31",
139
+ count: 2,
140
+ });
141
+ });
142
+
143
+ test("refuses source beyond the per-Package byte quota", async () => {
144
+ const refused = await reserveAuthoringQuotaV1(storage(), {
145
+ ...REQUEST,
146
+ sourceBytes: 256 * 1024 + 1,
147
+ });
148
+ expect(refused).toMatchObject({
149
+ status: "refused",
150
+ limitName: "source-bytes",
151
+ });
152
+ });
153
+
154
+ test("refuses once the Bot holds its retained-generation allowance", async () => {
155
+ const refused = await reserveAuthoringQuotaV1(storage(), {
156
+ ...REQUEST,
157
+ retainedGenerations: 50,
158
+ });
159
+ expect(refused).toMatchObject({
160
+ status: "refused",
161
+ limitName: "retained-generations",
162
+ limit: 50,
163
+ });
164
+ });
165
+
166
+ test("replays a refusal instead of letting a retry through", async () => {
167
+ const store = storage();
168
+ const first = await reserveAuthoringQuotaV1(store, {
169
+ ...REQUEST,
170
+ sourceBytes: 256 * 1024 + 1,
171
+ });
172
+ const second = await reserveAuthoringQuotaV1(store, {
173
+ ...REQUEST,
174
+ sourceBytes: 1,
175
+ });
176
+ expect(second).toEqual(first);
177
+ });
178
+
179
+ test("decodes its receipt at the Durable Object RPC seam", () => {
180
+ expect(
181
+ decodeAuthoringQuotaReceiptV1({
182
+ schemaVersion: 1,
183
+ status: "reserved",
184
+ effectId: "author-a",
185
+ day: "2026-08-31",
186
+ used: 1,
187
+ limit: 100,
188
+ }).status,
189
+ ).toBe("reserved");
190
+ expect(() =>
191
+ decodeAuthoringQuotaReceiptV1({ schemaVersion: 2, status: "reserved" }),
192
+ ).toThrow();
193
+ expect(() =>
194
+ decodeAuthoringQuotaReceiptV1({
195
+ schemaVersion: 1,
196
+ status: "refused",
197
+ effectId: "author-a",
198
+ day: "2026-08-31",
199
+ limitName: "unknown-limit",
200
+ reason: "no",
201
+ used: 1,
202
+ limit: 1,
203
+ }),
204
+ ).toThrow();
205
+ });
206
+
207
+ test("names the counter day in UTC", () => {
208
+ expect(authoringQuotaDayV1(new Date("2026-08-31T23:59:59.999Z"))).toBe(
209
+ "2026-08-31",
210
+ );
211
+ expect(() => authoringQuotaCounterKey("31-08-2026")).toThrow();
212
+ });
213
+ });