Skip to content

Commit 6b2689e

Browse files
committed
Keep CLI tests runnable across runtimes
Split the inbox and relay command parsers out of the runtime-heavy modules so source-level CLI tests do not import .tsx or node:sqlite under Node and Bun. This also rewrites the Deno version regression test to replace the global safely and raises the Bun timeout for vocab-tools so mise test stays green again. Assisted-by: OpenCode:gpt-5.4
1 parent 4b28b9a commit 6b2689e

7 files changed

Lines changed: 257 additions & 234 deletions

File tree

packages/cli/src/inbox.tsx

Lines changed: 7 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,18 @@ import {
2323
type Recipient,
2424
} from "@fedify/vocab";
2525
import { getLogger } from "@logtape/logtape";
26-
import { bindConfig } from "@optique/config";
27-
import {
28-
command,
29-
constant,
30-
group,
31-
type InferValue,
32-
merge,
33-
message,
34-
multiple,
35-
object,
36-
option,
37-
string,
38-
} from "@optique/core";
26+
import type { InferValue } from "@optique/core";
3927
import Table from "cli-table3";
4028
import { type Context as HonoContext, Hono } from "hono";
4129
import type { BlankEnv, BlankInput } from "hono/types";
4230
import process from "node:process";
4331
import ora from "ora";
4432
import metadata from "../deno.json" with { type: "json" };
45-
import { configContext } from "./config.ts";
4633
import { getDocumentLoader } from "./docloader.ts";
4734
import type { ActivityEntry } from "./inbox/entry.ts";
4835
import { ActivityEntryPage, ActivityListPage } from "./inbox/view.tsx";
4936
import { configureLogging, recordingSink } from "./log.ts";
50-
import { createTunnelOption, type GlobalOptions } from "./options.ts";
37+
import type { GlobalOptions } from "./options.ts";
5138
import { tableStyle } from "./table.ts";
5239
import { spawnTemporaryServer, type TemporaryServer } from "./tempserver.ts";
5340
import { colors, matchesActor } from "./utils.ts";
@@ -66,92 +53,18 @@ interface ContextData {
6653

6754
const logger = getLogger(["fedify", "cli", "inbox"]);
6855

69-
export const inboxCommand = command(
70-
"inbox",
71-
merge(
72-
object("Inbox options", {
73-
command: constant("inbox"),
74-
follow: bindConfig(
75-
multiple(
76-
option("-f", "--follow", string({ metavar: "URI" }), {
77-
description:
78-
message`Follow the given actor. The argument can be either an actor URI or a handle. Can be specified multiple times.`,
79-
}),
80-
),
81-
{
82-
context: configContext,
83-
key: (config) => config.inbox?.follow ?? [],
84-
default: [],
85-
},
86-
),
87-
acceptFollow: bindConfig(
88-
multiple(
89-
option("-a", "--accept-follow", string({ metavar: "URI" }), {
90-
description:
91-
message`Accept follow requests from the given actor. The argument can be either an actor URI or a handle, or a wildcard (${"*"}). Can be specified multiple times. If a wildcard is specified, all follow requests will be accepted.`,
92-
}),
93-
),
94-
{
95-
context: configContext,
96-
key: (config) => config.inbox?.acceptFollow ?? [],
97-
default: [],
98-
},
99-
),
100-
actorName: bindConfig(
101-
option("--actor-name", string({ metavar: "NAME" }), {
102-
description: message`Customize the actor display name.`,
103-
}),
104-
{
105-
context: configContext,
106-
key: (config) => config.inbox?.actorName ?? "Fedify Ephemeral Inbox",
107-
default: "Fedify Ephemeral Inbox",
108-
},
109-
),
110-
actorSummary: bindConfig(
111-
option("--actor-summary", string({ metavar: "SUMMARY" }), {
112-
description: message`Customize the actor description.`,
113-
}),
114-
{
115-
context: configContext,
116-
key: (config) =>
117-
config.inbox?.actorSummary ??
118-
"An ephemeral ActivityPub inbox for testing purposes.",
119-
default: "An ephemeral ActivityPub inbox for testing purposes.",
120-
},
121-
),
122-
authorizedFetch: bindConfig(
123-
option(
124-
"-A",
125-
"--authorized-fetch",
126-
{
127-
description:
128-
message`Enable authorized fetch mode. Incoming requests without valid HTTP signatures will be rejected with 401 Unauthorized.`,
129-
},
130-
),
131-
{
132-
context: configContext,
133-
key: (config) => config.inbox?.authorizedFetch ?? false,
134-
default: false,
135-
},
136-
),
137-
}),
138-
group("Tunnel options", createTunnelOption("inbox")),
139-
),
140-
{
141-
brief: message`Run an ephemeral ActivityPub inbox server.`,
142-
description:
143-
message`Spins up an ephemeral server that serves the ActivityPub inbox with an one-time actor, through a short-lived public DNS with HTTPS. You can monitor the incoming activities in real-time.`,
144-
},
145-
);
146-
14756
// Module-level state
14857
const activities: ActivityEntry[] = [];
14958
const acceptFollows: string[] = [];
15059
const peers: Record<string, Actor> = {};
15160
const followers: Record<string, Actor> = {};
15261

62+
type InboxCommand =
63+
& InferValue<typeof import("./inbox/command.ts").inboxCommand>
64+
& GlobalOptions;
65+
15366
export async function runInbox(
154-
command: InferValue<typeof inboxCommand> & GlobalOptions,
67+
command: InboxCommand,
15568
) {
15669
// Reset module-level state for a clean run
15770
activities.length = 0;

packages/cli/src/inbox/command.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { bindConfig } from "@optique/config";
2+
import {
3+
command,
4+
constant,
5+
group,
6+
merge,
7+
message,
8+
multiple,
9+
object,
10+
option,
11+
string,
12+
} from "@optique/core";
13+
import { configContext } from "../config.ts";
14+
import { createTunnelOption } from "../options.ts";
15+
16+
export const inboxCommand = command(
17+
"inbox",
18+
merge(
19+
object("Inbox options", {
20+
command: constant("inbox"),
21+
follow: bindConfig(
22+
multiple(
23+
option("-f", "--follow", string({ metavar: "URI" }), {
24+
description:
25+
message`Follow the given actor. The argument can be either an actor URI or a handle. Can be specified multiple times.`,
26+
}),
27+
),
28+
{
29+
context: configContext,
30+
key: (config) => config.inbox?.follow ?? [],
31+
default: [],
32+
},
33+
),
34+
acceptFollow: bindConfig(
35+
multiple(
36+
option("-a", "--accept-follow", string({ metavar: "URI" }), {
37+
description:
38+
message`Accept follow requests from the given actor. The argument can be either an actor URI or a handle, or a wildcard (${"*"}). Can be specified multiple times. If a wildcard is specified, all follow requests will be accepted.`,
39+
}),
40+
),
41+
{
42+
context: configContext,
43+
key: (config) => config.inbox?.acceptFollow ?? [],
44+
default: [],
45+
},
46+
),
47+
actorName: bindConfig(
48+
option("--actor-name", string({ metavar: "NAME" }), {
49+
description: message`Customize the actor display name.`,
50+
}),
51+
{
52+
context: configContext,
53+
key: (config) => config.inbox?.actorName ?? "Fedify Ephemeral Inbox",
54+
default: "Fedify Ephemeral Inbox",
55+
},
56+
),
57+
actorSummary: bindConfig(
58+
option("--actor-summary", string({ metavar: "SUMMARY" }), {
59+
description: message`Customize the actor description.`,
60+
}),
61+
{
62+
context: configContext,
63+
key: (config) =>
64+
config.inbox?.actorSummary ??
65+
"An ephemeral ActivityPub inbox for testing purposes.",
66+
default: "An ephemeral ActivityPub inbox for testing purposes.",
67+
},
68+
),
69+
authorizedFetch: bindConfig(
70+
option(
71+
"-A",
72+
"--authorized-fetch",
73+
{
74+
description:
75+
message`Enable authorized fetch mode. Incoming requests without valid HTTP signatures will be rejected with 401 Unauthorized.`,
76+
},
77+
),
78+
{
79+
context: configContext,
80+
key: (config) => config.inbox?.authorizedFetch ?? false,
81+
default: false,
82+
},
83+
),
84+
}),
85+
group("Tunnel options", createTunnelOption("inbox")),
86+
),
87+
{
88+
brief: message`Run an ephemeral ActivityPub inbox server.`,
89+
description:
90+
message`Spins up an ephemeral server that serves the ActivityPub inbox with an one-time actor, through a short-lived public DNS with HTTPS. You can monitor the incoming activities in real-time.`,
91+
},
92+
);

packages/cli/src/relay.ts

Lines changed: 6 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -2,140 +2,25 @@ import { MemoryKvStore } from "@fedify/fedify";
22
import { createRelay, type Relay, type RelayType } from "@fedify/relay";
33
import { SqliteKvStore } from "@fedify/sqlite";
44
import { getLogger } from "@logtape/logtape";
5-
import { bindConfig } from "@optique/config";
6-
import {
7-
command,
8-
constant,
9-
group,
10-
type InferValue,
11-
integer,
12-
merge,
13-
message,
14-
multiple,
15-
object,
16-
option,
17-
optional,
18-
optionName,
19-
string,
20-
value,
21-
} from "@optique/core";
22-
import { choice } from "@optique/core/valueparser";
5+
import type { InferValue } from "@optique/core";
236
import Table from "cli-table3";
247
import process from "node:process";
258
import { DatabaseSync } from "node:sqlite";
269
import ora from "ora";
27-
import { configContext } from "./config.ts";
2810
import { configureLogging } from "./log.ts";
29-
import { createTunnelOption, type GlobalOptions } from "./options.ts";
11+
import type { GlobalOptions } from "./options.ts";
3012
import { tableStyle } from "./table.ts";
3113
import { spawnTemporaryServer, type TemporaryServer } from "./tempserver.ts";
3214
import { colors, matchesActor } from "./utils.ts";
3315

3416
const logger = getLogger(["fedify", "cli", "relay"]);
3517

36-
export const relayCommand = command(
37-
"relay",
38-
merge(
39-
object("Relay options", {
40-
command: constant("relay"),
41-
protocol: bindConfig(
42-
option(
43-
"-p",
44-
"--protocol",
45-
choice(["mastodon", "litepub"], { metavar: "TYPE" }),
46-
{
47-
description: message`The relay protocol to use. ${
48-
value("mastodon")
49-
} for Mastodon-compatible relay, ${
50-
value("litepub")
51-
} for LitePub-compatible relay.`,
52-
},
53-
),
54-
{
55-
context: configContext,
56-
key: (config) => config.relay?.protocol ?? "mastodon",
57-
default: "mastodon",
58-
},
59-
),
60-
persistent: optional(
61-
bindConfig(
62-
option("--persistent", string({ metavar: "PATH" }), {
63-
description:
64-
message`Path to SQLite database file for persistent storage. If not specified, uses in-memory storage which is lost when the server stops.`,
65-
}),
66-
{
67-
context: configContext,
68-
key: (config) => config.relay?.persistent,
69-
},
70-
),
71-
),
72-
port: bindConfig(
73-
option(
74-
"-P",
75-
"--port",
76-
integer({ min: 0, max: 65535, metavar: "PORT" }),
77-
{
78-
description: message`The local port to listen on.`,
79-
},
80-
),
81-
{
82-
context: configContext,
83-
key: (config) => config.relay?.port ?? 8000,
84-
default: 8000,
85-
},
86-
),
87-
name: bindConfig(
88-
option("-n", "--name", string({ metavar: "NAME" }), {
89-
description: message`The relay display name.`,
90-
}),
91-
{
92-
context: configContext,
93-
key: (config) => config.relay?.name ?? "Fedify Relay",
94-
default: "Fedify Relay",
95-
},
96-
),
97-
acceptFollow: bindConfig(
98-
multiple(
99-
option("-a", "--accept-follow", string({ metavar: "URI" }), {
100-
description:
101-
message`Accept follow requests from the given actor. The argument can be either an actor URI or a handle, or a wildcard (${"*"}). Can be specified multiple times. If a wildcard is specified, all follow requests will be accepted.`,
102-
}),
103-
),
104-
{
105-
context: configContext,
106-
key: (config) => config.relay?.acceptFollow ?? [],
107-
default: [],
108-
},
109-
),
110-
rejectFollow: bindConfig(
111-
multiple(
112-
option("-r", "--reject-follow", string({ metavar: "URI" }), {
113-
description:
114-
message`Reject follow requests from the given actor. The argument can be either an actor URI or a handle, or a wildcard (${"*"}). Can be specified multiple times. If a wildcard is specified, all follow requests will be rejected.`,
115-
}),
116-
),
117-
{
118-
context: configContext,
119-
key: (config) => config.relay?.rejectFollow ?? [],
120-
default: [],
121-
},
122-
),
123-
}),
124-
group("Tunnel options", createTunnelOption("relay")),
125-
),
126-
{
127-
brief: message`Run an ephemeral ActivityPub relay server.`,
128-
description:
129-
message`Spins up an ActivityPub relay server that forwards activities between federated instances. The server can use either Mastodon or LitePub compatible relay protocol.
130-
131-
By default, the server is tunneled to the public internet for external access. Use ${
132-
optionName("--no-tunnel")
133-
} to run locally only.`,
134-
},
135-
);
18+
type RelayCommand =
19+
& InferValue<typeof import("./relay/command.ts").relayCommand>
20+
& GlobalOptions;
13621

13722
export async function runRelay(
138-
command: InferValue<typeof relayCommand> & GlobalOptions,
23+
command: RelayCommand,
13924
): Promise<void> {
14025
if (command.debug) {
14126
await configureLogging();

0 commit comments

Comments
 (0)