mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
feat(discord): make the typed first-message hint a separate opt-in, off by default
Claude keeps the silent system-prompt hint (startupHint, on). Other CLIs are sent the typed message only with typedHint, which costs a model turn per tab.
This commit is contained in:
1 parent
e95d38a944
commit
6d4357502b
10 files changed
+77
-27
No files matched your search
@@ -131,7 +131,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
|
||||
|
||||
**Agent CLI (`codeman agent …`, `src/cli-agent.ts`)**: the command-line client for the wait primitives and the session verbs, for agents the skill does not reach (pi, shell, no `jq`; `seedAgentSessionPreamble` covers every local session whose CLI takes the skill, `capabilities.agentSkillDir`; every pane still exports `CODEMAN_MUX`/`CODEMAN_SESSION_ID`/`CODEMAN_API_URL`). It is an HTTP CLIENT: inspection and guarded stage/submit use the ownership-checked `agent-session-routes.ts` module (experimental, behind `agentCoordinationEnabled`, SYNCED, default OFF: all three routes answer `403 FORBIDDEN` naming it while off, and the CLI shows that verbatim); the other verbs reuse existing routes, with no second transport (the plan's "Socket API" non-goal stands), every call carries `X-Codeman-Parent-Session`, only `spawn`'s quick-start carries `X-Codeman-Agent-Origin` (pinned in test/cli-agent.test.ts), while `spawn` without a case (cwd or `--working-dir`) is `POST /api/v1/sessions` plus `/interactive` or `/shell` without that header, so no agent-case marker, and refuses (exit 4, before any request) an empty `--working-dir` and a bare spawn from a HOME cwd, since it always sends an explicit `workingDir` and the server's no-`workingDir` hooks skip cannot apply (test/cli-agent-project-spawn.test.ts), auth (`src/codeman-credentials.ts`, shared with `codeman attach` and the TUI) from `CODEMAN_PASSWORD` or the data dir's `.env` (the same reader `codeman attach` uses — `readCodemanEnvFile`). Verbs return exit codes (`0` ok / `1` error / `2` timeout / `3` exited / `4` refused) instead of calling `process.exit`, so `test/cli-agent.test.ts` drives every verb against a recording fake transport plus the real `httpRequest` against a local server. ⚠️ Three guards live in code, not prose, and the tests pin them: refuse without `CODEMAN_MUX=1`+`CODEMAN_API_URL` (never guess a URL); `send` transmits printable text + `\r` only (`inputRefusal`: any byte < 0x20 or DEL refuses BEFORE the transport — opencode's `Ctrl+C` is `app_exit`), ESC exists solely as `interrupt` with no `\r`; `rm` fails closed (`deleteRefusal`: empty id, self shorter than 8, prefix match in EITHER direction, re-checked on the resolved id). ⚠️ The routes accept FULL ids only — an 8-char prefix is a 404 (measured live) — while `ls` prints prefixes, so `resolveSessionId` maps a short id through `GET /api/v1/sessions` and refuses an ambiguous one. It also refuses any id under 8 characters (exit 4, on every verb, before any request), the same floor as the server's `PARENT_SESSION_ID_MIN_PREFIX`, so `rm 9` can never pick an arbitrary session. ⚠️ A 400 for `--until stop` on a hook-less mode is passed through verbatim, never papered over: the marker path is the answer there. `spawn` on a readiness timeout exits 2 and LEAVES the session for inspection (the skill's `spawn_worker` deletes it; the CLI's caller is often a human); its stdout is the id ALONE (prose on stderr) so `SID=$(…)` works. ⚠️ Two server answers that look like success and are not: `delivered:false` without `duplicate` (the bytes went into a dead pane — the field exists so a client says "restart" instead of "wait longer") and `wait.ended:true, signal:null` (the worker died during a `stop`/`idle` wait: the registry satisfies only waiters that listed `exit`, then cancels the rest) — both exit 3, checked BEFORE the happy paths in `waitExitCode`. → README "`codeman agent`", `docs/api-reference.md`
|
||||
|
||||
**Discord bridge (`src/discord-bridge.ts`, `src/web/routes/discord-routes.ts`, `src/cli-notify.ts`)**: opt-in two-way Discord (a bot, not the one-way webhook), one thread per tab. Split like `webhook-notify.ts`: pure helpers, a 0600 store (`discord.json` holds the bot token and is never returned; `discord-bridge.json` maps sessionId to thread id), then IO (`DiscordApi` REST with injected fetch, `DiscordGateway` inbound WebSocket with injected socket, `DiscordBridge` rules). Outbound hangs off `WebServer.sendPushNotifications` beside the webhook (so it works with zero push subscriptions) and tab events off `WebServer.broadcast` (`bridgeSessionEvent`); the alert tail is the RENDERED tmux screen (`screenTail`), never the raw stream, which a full-screen UI repaints in place. Inbound replies are typed through `session.writeViaMux`. ⚠️ Guards that live in code and tests pin: only `allowedUserIds`, in `guildId`, inside a thread the bridge created, are acted on, everything else is dropped unread; a reply is one printable line plus Enter, capped and rate limited; posts send `allowed_mentions` limited to the allowlisted ids; received files go only from Discord's own HTTPS hosts (max 5 x 8 MB, names reduced to `[A-Za-z0-9._-]`) into `<tab folder>/.codeman-discord/`, and a sent file's REAL path must lie inside the tab's folder (`fileProblem`, symlinks followed); a Discord failure never reaches a session; the alert tail is a function the bridge calls only once it will post (no tmux exec per event while off); a delete reaches the bridge from the cleanup path with `killMux` true, never from `session:deleted`, which a detach also fires; `.codeman-discord/` is probed with the bounded probe, refused if a symlink, and ignores itself with a `.gitignore` of `*`; a config restore of `discord.json` reloads the running bridge (`afterRestore`). The startup hint ("how to reach the user") rides the CLI registry as the engine value `appendSystemPrompt` (Claude's `--append-system-prompt`, undefined unless the bridge and `startupHint` are on, so every other launch is byte-identical) and is typed once as a first message to brand-new external CLI tabs on their first idle (`sessionIdle`; never a shell, a restored tab, a resumed one, or a remote (ssh) or Docker tab, which have no `codeman` on their PATH). `codeman notify "<message>" [--urgent] [--file <path>]` wraps `POST /api/sessions/:id/discord-notify`, which accepts a unique 8-character id prefix (a Docker case exports it cut). → `docs/discord-bridge.md`, `docs/wiki/Discord-Bridge.md`, `docs/api-reference.md`
|
||||
**Discord bridge (`src/discord-bridge.ts`, `src/web/routes/discord-routes.ts`, `src/cli-notify.ts`)**: opt-in two-way Discord (a bot, not the one-way webhook), one thread per tab. Split like `webhook-notify.ts`: pure helpers, a 0600 store (`discord.json` holds the bot token and is never returned; `discord-bridge.json` maps sessionId to thread id), then IO (`DiscordApi` REST with injected fetch, `DiscordGateway` inbound WebSocket with injected socket, `DiscordBridge` rules). Outbound hangs off `WebServer.sendPushNotifications` beside the webhook (so it works with zero push subscriptions) and tab events off `WebServer.broadcast` (`bridgeSessionEvent`); the alert tail is the RENDERED tmux screen (`screenTail`), never the raw stream, which a full-screen UI repaints in place. Inbound replies are typed through `session.writeViaMux`. ⚠️ Guards that live in code and tests pin: only `allowedUserIds`, in `guildId`, inside a thread the bridge created, are acted on, everything else is dropped unread; a reply is one printable line plus Enter, capped and rate limited; posts send `allowed_mentions` limited to the allowlisted ids; received files go only from Discord's own HTTPS hosts (max 5 x 8 MB, names reduced to `[A-Za-z0-9._-]`) into `<tab folder>/.codeman-discord/`, and a sent file's REAL path must lie inside the tab's folder (`fileProblem`, symlinks followed); a Discord failure never reaches a session; the alert tail is a function the bridge calls only once it will post (no tmux exec per event while off); a delete reaches the bridge from the cleanup path with `killMux` true, never from `session:deleted`, which a detach also fires; `.codeman-discord/` is probed with the bounded probe, refused if a symlink, and ignores itself with a `.gitignore` of `*`; a config restore of `discord.json` reloads the running bridge (`afterRestore`). The startup hint ("how to reach the user") rides the CLI registry as the engine value `appendSystemPrompt` (Claude's `--append-system-prompt`, undefined unless the bridge and `startupHint` are on, so every other launch is byte-identical) and, only with the separate `typedHint` setting (default OFF), is typed once as a first message to brand-new external CLI tabs on their first idle (`sessionIdle`; never a shell, a restored tab, a resumed one, or a remote (ssh) or Docker tab, which have no `codeman` on their PATH). `codeman notify "<message>" [--urgent] [--file <path>]` wraps `POST /api/sessions/:id/discord-notify`, which accepts a unique 8-character id prefix (a Docker case exports it cut). → `docs/discord-bridge.md`, `docs/wiki/Discord-Bridge.md`, `docs/api-reference.md`
|
||||
|
||||
**MCP server (`codeman mcp`, `src/mcp-server.ts`)**: a Model Context Protocol server on stdio for any MCP client, a CLIENT of the HTTP API like `codeman agent` (reuses its `httpRequest`, `inputRefusal`, `buildSendBody`; the MCP server adds no routes). Seven tools: `list_sessions`, `start_session`, `send_prompt`, `wait_for_session`, `read_output`, `interrupt_session`, `delete_session`. ⚠️ Dual-era: answers the legacy `initialize` handshake (2024-11-05 to 2025-11-25) AND the 2026-07-28 revision (per-request `_meta` with `protocolVersion` + `clientCapabilities`, `server/discover`, `resultType: "complete"` on every modern result); a modern `tools/list`/`server/discover` MUST carry `ttlMs` + `cacheScope` (Claude Code 2.1.296 drops every tool without them: measured). ⚠️ stdout is protocol only; logs go to stderr. ⚠️ The URL comes from `--url`/`CODEMAN_API_URL`, never a guess, and unconfigured it still lists tools (registries introspect) while every call answers how to configure it. ⚠️ Prompt/interrupt/delete only sessions the PROCESS started (in-memory allowlist, so a restart fails closed) or, inside a pane, the calling session's own children, unless `--allow-any-session`; never its own session. ⚠️ `read_output` returns text ONLY, no `structuredContent` (a client that prefers it showed the model metadata instead of the output: measured). Waits default 45 s (Codex's MCP tool timeout is 60 s). Registry manifest `config/mcp-registry/server.json` (`mcpName` in package.json, version kept by `scripts/sync-plugin.mjs`). Tests: `test/mcp-server.test.ts`.
|
||||
|
||||
|
||||
@@ -1493,7 +1493,7 @@ One thread per tab in a private Discord channel, alerts out and replies typed ba
|
||||
| Method | Path | Body | Notes |
|
||||
| ------ | ------ | ------ | ------ |
|
||||
| `GET` | `/api/discord` | none | Config without the token, plus `hasToken`, `tokenMasked`, `gateway` (`off` \| `connecting` \| `connected` \| `error`), `gatewayError` and `lastResult`. |
|
||||
| `PUT` | `/api/discord` | `{ enabled?, token?, guildId?, parentChannelId?, allowedUserIds?, threadCreation?, onSessionDelete?, pingMode?, scope?, startupHint? }` (strict) | `threadCreation`: `lazy` \| `onSessionStart`. `onSessionDelete`: `keep` \| `archive` \| `delete`. `pingMode`: `off` \| `critical` \| `all`. `scope`: `attention` \| `all`. An absent `token` keeps the saved one; `""` clears it. `400` for a non-numeric id or enabling without a token, guild, channel and at least one allowed user. |
|
||||
| `PUT` | `/api/discord` | `{ enabled?, token?, guildId?, parentChannelId?, allowedUserIds?, threadCreation?, onSessionDelete?, pingMode?, scope?, startupHint?, typedHint? }` (strict) | `threadCreation`: `lazy` \| `onSessionStart`. `onSessionDelete`: `keep` \| `archive` \| `delete`. `pingMode`: `off` \| `critical` \| `all`. `scope`: `attention` \| `all`. An absent `token` keeps the saved one; `""` clears it. `400` for a non-numeric id or enabling without a token, guild, channel and at least one allowed user. |
|
||||
| `POST` | `/api/discord/test` | none | Posts one message to the parent channel with the saved config, even while disabled. `200` with `data.ok`; `400` if no token or channel is saved. |
|
||||
| `POST` | `/api/sessions/:id/discord-notify` | `{ message, urgent?, file? }` (strict, message 1 to 1500 characters) | A CLI asks for the user: posts into that tab's thread, pinging when `urgent`. `file` is an absolute path inside the tab's working folder (real path, regular file, at most 8 MB, local sessions only) uploaded with the post; `400` otherwise. `409` while the bridge is disabled. The `codeman notify "message" [--urgent] [--file path]` command wraps this route. |
|
||||
|
||||
|
||||
+11
-10
@@ -88,20 +88,21 @@ In the Discord mobile app set the server to **All Messages**. Discord's own push
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Values | Default | Effect |
|
||||
| ----------------- | --------------------------- | ----------- | ---------------------------------------------------------------------------------------- |
|
||||
| `threadCreation` | `lazy`, `onSessionStart` | `lazy` | `lazy` makes a thread on the first notification. `onSessionStart` makes one per new tab |
|
||||
| `onSessionDelete` | `keep`, `archive`, `delete` | `archive` | What happens to the thread when its tab is deleted. `delete` needs Manage Threads |
|
||||
| `pingMode` | `off`, `critical`, `all` | `critical` | `critical` pings you for permission prompts, questions, errors and urgent posts |
|
||||
| `scope` | `attention`, `all` | `attention` | `attention` skips the quiet "response complete" event. `all` sends it too |
|
||||
| `startupHint` | on, off | on | Tell each CLI about `codeman notify` (Claude at launch, others once on their first idle) |
|
||||
| Option | Values | Default | Effect |
|
||||
| ----------------- | --------------------------- | ----------- | --------------------------------------------------------------------------------------- |
|
||||
| `threadCreation` | `lazy`, `onSessionStart` | `lazy` | `lazy` makes a thread on the first notification. `onSessionStart` makes one per new tab |
|
||||
| `onSessionDelete` | `keep`, `archive`, `delete` | `archive` | What happens to the thread when its tab is deleted. `delete` needs Manage Threads |
|
||||
| `pingMode` | `off`, `critical`, `all` | `critical` | `critical` pings you for permission prompts, questions, errors and urgent posts |
|
||||
| `scope` | `attention`, `all` | `attention` | `attention` skips the quiet "response complete" event. `all` sends it too |
|
||||
| `startupHint` | on, off | on | Tell Claude about `codeman notify` in its system prompt at launch |
|
||||
| `typedHint` | on, off | off | Also send the note once, as a first message, to brand-new non-Claude CLI tabs |
|
||||
|
||||
## Telling the CLIs
|
||||
|
||||
A CLI only posts deliberately if it knows it can. While the bridge and `startupHint` are on:
|
||||
A CLI only posts deliberately if it knows it can. While the bridge is on:
|
||||
|
||||
- **Claude** is launched with `--append-system-prompt`, so nothing shows in the conversation.
|
||||
- **Every other CLI** is sent the same text once, as a message, the first time a brand-new tab becomes idle. Tabs that are restored when Codeman starts, resumed tabs, shell tabs, and remote (ssh) and Docker tabs (which have no `codeman` command) are never sent it.
|
||||
- **Claude** (`startupHint`, on by default) is launched with `--append-system-prompt`, so nothing shows in the conversation.
|
||||
- **Every other CLI** is sent nothing unless you turn on `typedHint` (off by default: it costs one model turn per tab, and for a spawned worker or a scheduled run it arrives together with the first real prompt). With it on, the same text is sent once, as a message, the first time a brand-new tab becomes idle. Tabs that are restored when Codeman starts, resumed tabs, shell tabs, and remote (ssh) and Docker tabs (which have no `codeman` command) are never sent it.
|
||||
|
||||
The text explains `codeman notify "message" --urgent`. `codeman` is already on the PATH of every session and reads `CODEMAN_SESSION_ID` and `CODEMAN_API_URL`, so no other setup is needed. Without a bridge it prints that the bridge is off and exits cleanly. Tabs started before the bridge was enabled are not told; use the skill or `CLAUDE.md` for those.
|
||||
|
||||
|
||||
+11
-10
@@ -38,23 +38,24 @@ only), never in the shared settings, and never shown again once saved.
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Choices | Default |
|
||||
| ------------------------------ | ---------------------------------------- | --------------- |
|
||||
| Create threads | On the first alert, or for every new tab | First alert |
|
||||
| When a tab is deleted | Keep, archive or delete the thread | Archive |
|
||||
| @ ping | Never, critical events only, every post | Critical only |
|
||||
| Which events | Needs attention, or everything | Needs attention |
|
||||
| Tell each CLI how to reach you | On or off | On |
|
||||
| Option | Choices | Default |
|
||||
| ---------------------------------- | ---------------------------------------- | --------------- |
|
||||
| Create threads | On the first alert, or for every new tab | First alert |
|
||||
| When a tab is deleted | Keep, archive or delete the thread | Archive |
|
||||
| @ ping | Never, critical events only, every post | Critical only |
|
||||
| Which events | Needs attention, or everything | Needs attention |
|
||||
| Tell Claude how to reach you | On or off | On |
|
||||
| Also tell other CLIs, as a message | On or off | Off |
|
||||
|
||||
Deleting a thread needs the **Manage Threads** permission on the channel. "Critical" means a
|
||||
permission prompt, a question, an error, or a post an agent marked urgent.
|
||||
|
||||
## Telling the agents
|
||||
|
||||
An agent only posts deliberately if it knows it can. With **Tell each CLI how to reach you** on:
|
||||
An agent only posts deliberately if it knows it can:
|
||||
|
||||
- **Claude** is launched with the instruction as part of its system prompt, so nothing shows in the chat.
|
||||
- **Every other CLI** is sent the same text once, as a message, the first time a brand-new tab goes idle.
|
||||
- **Claude** is launched with the instruction as part of its system prompt (**Tell Claude how to reach you**, on by default), so nothing shows in the chat.
|
||||
- **Every other CLI** is told only if you turn on **Also tell other CLIs, as a message** (off by default: it costs one model turn per tab, and for a spawned worker or a scheduled run it arrives with the first real prompt). Then the same text is sent once, as a message, the first time a brand-new tab goes idle.
|
||||
Restored tabs, resumed tabs, shell tabs, and remote (ssh) and Docker tabs (which have no `codeman` command) are never sent it.
|
||||
|
||||
Tabs that were already open when you turned the bridge on are not told. The alerts still reach
|
||||
|
||||
@@ -63,6 +63,11 @@ export interface DiscordConfig {
|
||||
scope: WebhookScope;
|
||||
/** Tell each session how to reach the user (see DISCORD_STARTUP_HINT). On by default while the bridge is on. */
|
||||
startupHint: boolean;
|
||||
/**
|
||||
* Also type the hint, once, as the first message to a brand-new non-Claude CLI tab. Off by default: it spends a
|
||||
* model turn per tab, and for a spawned worker or a cron session it lands on the same first idle as the real prompt.
|
||||
*/
|
||||
typedHint: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_DISCORD_CONFIG: DiscordConfig = {
|
||||
@@ -76,6 +81,7 @@ export const DEFAULT_DISCORD_CONFIG: DiscordConfig = {
|
||||
pingMode: 'critical',
|
||||
scope: 'attention',
|
||||
startupHint: true,
|
||||
typedHint: false,
|
||||
};
|
||||
|
||||
export interface DiscordMessage {
|
||||
@@ -271,6 +277,7 @@ function coerce(raw: unknown): DiscordConfig {
|
||||
pingMode: oneOf(DISCORD_PING_MODES, r.pingMode, d.pingMode),
|
||||
scope: oneOf(WEBHOOK_SCOPES, r.scope, d.scope),
|
||||
startupHint: r.startupHint === undefined ? d.startupHint : r.startupHint === true,
|
||||
typedHint: r.typedHint === true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -859,7 +866,7 @@ export class DiscordBridge {
|
||||
*/
|
||||
async sessionIdle(sessionId: string, hintable: boolean): Promise<void> {
|
||||
if (!this.fresh.delete(sessionId)) return;
|
||||
if (!hintable || !this.cfg.enabled || !this.cfg.startupHint) return;
|
||||
if (!hintable || !this.cfg.enabled || !this.cfg.startupHint || !this.cfg.typedHint) return;
|
||||
const text = sanitiseReply(`${DISCORD_STARTUP_HINT} Reply with just: ok`);
|
||||
// fromUser false: this is Codeman talking, so it must not auto-name the tab or count as the person's input.
|
||||
if (text) await this.deps.sendInput(sessionId, text, false).catch(() => false);
|
||||
|
||||
@@ -3288,11 +3288,18 @@
|
||||
</div>
|
||||
<div class="set-row">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Tell each CLI how to reach you</span>
|
||||
<span class="set-row-desc">Claude is told at launch. Other CLIs are sent one short message when a new tab first becomes idle. It explains the codeman notify command, for when a CLI is blocked on something only you can do. Never sent to shell tabs.</span>
|
||||
<span class="set-row-label">Tell Claude how to reach you</span>
|
||||
<span class="set-row-desc">Adds a short note about the codeman notify command to Claude's system prompt at launch, for when it is blocked on something only you can do. Nothing appears in the conversation.</span>
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="discordStartupHint"><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="set-row">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Also tell other CLIs, as a first message</span>
|
||||
<span class="set-row-desc">Sends the same note once as a message to a brand-new Codex, OpenCode or similar tab when it first becomes idle. Costs one model turn per tab, and for a spawned worker or a scheduled run it arrives with the first real prompt. Never sent to shell, restored, resumed, remote or Docker tabs. Off by default.</span>
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="discordTypedHint"><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="set-row has-field">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Which events</span>
|
||||
|
||||
@@ -1770,6 +1770,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
document.getElementById('discordPingMode').value = d.pingMode;
|
||||
document.getElementById('discordScope').value = d.scope;
|
||||
document.getElementById('discordStartupHint').checked = d.startupHint !== false;
|
||||
document.getElementById('discordTypedHint').checked = d.typedHint === true;
|
||||
const clearBtn = document.getElementById('discordClearBtn');
|
||||
if (clearBtn) clearBtn.style.display = d.hasToken ? '' : 'none';
|
||||
const gw = { off: 'Not connected (disabled).', connecting: 'Connecting to Discord…', connected: 'Connected: replies are being read.', error: `Not connected: ${d.gatewayError || 'error'}` };
|
||||
@@ -1794,6 +1795,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
pingMode: document.getElementById('discordPingMode').value,
|
||||
scope: document.getElementById('discordScope').value,
|
||||
startupHint: document.getElementById('discordStartupHint').checked,
|
||||
typedHint: document.getElementById('discordTypedHint').checked,
|
||||
};
|
||||
const token = document.getElementById('discordToken').value.trim();
|
||||
if (token) payload.token = token; // blank = keep the saved one (Remove token is the way to clear it)
|
||||
|
||||
@@ -2232,6 +2232,7 @@ export const DiscordUpdateSchema = z
|
||||
pingMode: z.enum(DISCORD_PING_MODES).optional(),
|
||||
scope: z.enum(WEBHOOK_SCOPES).optional(),
|
||||
startupHint: z.boolean().optional(),
|
||||
typedHint: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
@@ -3,7 +3,16 @@
|
||||
* @fileoverview Discord bridge: pure rules, the 0600 store, the REST client, the gateway state
|
||||
* machine (fake socket) and the bridge's gates. No network, no real Discord.
|
||||
*/
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
@@ -634,7 +643,7 @@ describe('DiscordBridge files and startup hint', () => {
|
||||
now: () => t,
|
||||
sleep: async () => undefined,
|
||||
});
|
||||
return writeDiscordConfig(dir, { ...CFG, ...cfg }).then(async () => {
|
||||
return writeDiscordConfig(dir, { ...CFG, typedHint: true, ...cfg }).then(async () => {
|
||||
await b.reload();
|
||||
await b.notify(MSG);
|
||||
posted.length = 0;
|
||||
@@ -730,6 +739,15 @@ describe('DiscordBridge files and startup hint', () => {
|
||||
b.stop();
|
||||
});
|
||||
|
||||
it('sends the typed hint only when typedHint is on (the default is off)', async () => {
|
||||
const off = await make({ typedHint: false });
|
||||
await off.sessionCreated('s7', 'new', t - 1000);
|
||||
await off.sessionIdle('s7', true);
|
||||
expect(sent).toHaveLength(0);
|
||||
off.stop();
|
||||
expect(DEFAULT_DISCORD_CONFIG.typedHint).toBe(false);
|
||||
});
|
||||
|
||||
it('does not hint a restored (old) session, or when startupHint is off', async () => {
|
||||
const b = await make();
|
||||
await b.sessionCreated('s4', 'old', 1); // created long ago: restored at server start
|
||||
|
||||
@@ -79,6 +79,19 @@ describe('/api/discord', () => {
|
||||
bridge.stop();
|
||||
});
|
||||
|
||||
it('stores startupHint (default on) and typedHint (default off) and returns them', async () => {
|
||||
const { app, bridge } = await harness();
|
||||
const first = (await app.inject({ method: 'GET', url: '/api/discord' })).json().data;
|
||||
expect(first).toMatchObject({ startupHint: true, typedHint: false });
|
||||
const put = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/discord',
|
||||
payload: { typedHint: true, startupHint: false },
|
||||
});
|
||||
expect(put.json().data).toMatchObject({ startupHint: false, typedHint: true });
|
||||
bridge.stop();
|
||||
});
|
||||
|
||||
it('refuses a bad id, an incomplete enable, and unknown fields', async () => {
|
||||
const { app } = await harness();
|
||||
const bad = await app.inject({ method: 'PUT', url: '/api/discord', payload: { guildId: 'not-a-number' } });
|
||||
|
||||
Reference in new issue
Block a user