1import { assert, describe, it } from "@effect/vitest";
2import * as Schema from "effect/Schema";
3
4import { ThreadMetadataMcpUpdateInput } from "./threadMetadataMcp.ts";
5
6const decodeUpdate = Schema.decodeUnknownSync(ThreadMetadataMcpUpdateInput);
7
8describe("ThreadMetadataMcpUpdateInput", () => {
9 it("decodes each metadata action with only its required fields", () => {
10 assert.deepEqual(decodeUpdate({ action: "rename", title: "Release follow-up" }), {
11 action: "rename",
12 title: "Release follow-up",
13 });
14 assert.deepEqual(decodeUpdate({ action: "regenerate_title" }), {
15 action: "regenerate_title",
16 });
17 assert.deepEqual(
18 decodeUpdate({
19 action: "link_pull_request",
20 pullRequest: {
21 repository: "pingdotgg/t3code",
22 number: 8689,
23 url: "https://github.com/pingdotgg/t3code/pull/8689",
24 },
25 }),
26 {
27 action: "link_pull_request",
28 pullRequest: {
29 repository: "pingdotgg/t3code",
30 number: 8689,
31 url: "https://github.com/pingdotgg/t3code/pull/8689",
32 },
33 },
34 );
35 assert.deepEqual(decodeUpdate({ action: "unlink_pull_request" }), {
36 action: "unlink_pull_request",
37 });
38 });
39
40 it("rejects missing action data and fields from another action", () => {
41 assert.throws(() => decodeUpdate({ action: "rename" }));
42 assert.throws(() => decodeUpdate({ action: "regenerate_title", title: "Not allowed" }));
43 assert.throws(() => decodeUpdate({ action: "link_pull_request" }));
44 assert.throws(() =>
45 decodeUpdate({
46 action: "unlink_pull_request",
47 pullRequest: {
48 repository: "pingdotgg/t3code",
49 number: 8689,
50 url: "https://github.com/pingdotgg/t3code/pull/8689",
51 },
52 }),
53 );
54 });
55
56 it("rejects unknown actions", () => {
57 assert.throws(() => decodeUpdate({ action: "move_workspace" }));
58 });
59
60 it("rejects malformed client request ids without normalizing valid keys", () => {
61 const validKey = "metadata-\ud83d\ude80-1";
62 assert.equal(
63 decodeUpdate({ action: "regenerate_title", clientRequestId: validKey }).clientRequestId,
64 validKey,
65 );
66 assert.throws(() =>
67 decodeUpdate({ action: "regenerate_title", clientRequestId: "metadata-\ud800" }),
68 );
69 assert.throws(() =>
70 decodeUpdate({ action: "regenerate_title", clientRequestId: "metadata-\udc00" }),
71 );
72 });
73
74 it("accepts HTTP(S) pull request URLs and rejects other or malformed URLs", () => {
75 assert.deepEqual(
76 decodeUpdate({
77 action: "link_pull_request",
78 pullRequest: {
79 repository: "engineering/t3code",
80 number: 42,
81 url: "https://git.corp.example/engineering/t3code/pulls/42",
82 },
83 }).pullRequest,
84 {
85 repository: "engineering/t3code",
86 number: 42,
87 url: "https://git.corp.example/engineering/t3code/pulls/42",
88 },
89 );
90
91 for (const url of ["not a URL", "javascript:alert(1)", "file:///tmp/pull-request"]) {
92 assert.throws(() =>
93 decodeUpdate({
94 action: "link_pull_request",
95 pullRequest: { repository: "pingdotgg/t3code", number: 8690, url },
96 }),
97 );
98 }
99 });
100});