fix(discord): digit replies without Enter, start the gateway after restore, hide other users' session ids, fix docs

- a lone digit reply is typed without Enter while the tab has a pending permission or question
- the gateway starts after restoreMuxSessions (hold and release), so a reply during restore is not told the tab is gone
- discord-notify resolves an id prefix only among sessions the caller may see
- restore docs/architecture-invariants.md from base and add only the Discord section
- docs: self-ignoring folder, no logging, Notifications group, Send test, local-tab-only Claude hint, typedHint wording
This commit is contained in:
Devvyn committed 2026-10-30 07:58:33 +08:00
1 parent dc22519adb
commit 212989b349
10 files changed
+120 -49

No files matched your search

+31 -1
View File
@@ -541,6 +541,16 @@ describe('DiscordBridge', () => {
b.stop();
});
it('keeps the gateway off while held (boot) and starts it on release', async () => {
const b = bridge();
b.hold();
await b.reload();
expect(b.gatewayState).toBe('off');
await b.release();
expect(b.gatewayState).not.toBe('off');
b.stop();
});
describe('sendTest', () => {
it('posts with the config it is given and leaves the live config alone', async () => {
const auth: string[] = [];
@@ -721,7 +731,11 @@ describe('DiscordBridge files and startup hint', () => {
let posted: Array<{ path: string; body?: Record<string, unknown> }>;
let downloads: string[];
function make(cfg: Partial<DiscordConfig> = {}, dirFor: (id: string) => string | undefined = () => work) {
function make(
cfg: Partial<DiscordConfig> = {},
dirFor: (id: string) => string | undefined = () => work,
awaitingChoice?: (id: string) => boolean
) {
const b = new DiscordBridge({
configDir: dir,
fetchImpl: async (url, init) => {
@@ -736,6 +750,7 @@ describe('DiscordBridge files and startup hint', () => {
},
sendInput: async (id, text, fromUser) => (sent.push([id, text, fromUser]), true),
sessionDir: dirFor,
awaitingChoice,
socketFactory: () => new FakeSocket(),
now: () => t,
sleep: async () => undefined,
@@ -819,6 +834,21 @@ describe('DiscordBridge files and startup hint', () => {
b.stop();
});
it('types a lone digit without Enter while a dialog is waiting, and with Enter otherwise', async () => {
let waiting = true;
const b = await make({}, undefined, () => waiting);
await b.onGatewayMessage(reply({ content: '2', attachments: [] }));
expect(sent.at(-1)?.[1]).toBe('2');
t += 5_000;
await b.onGatewayMessage(reply({ content: 'yes', attachments: [] }));
expect(sent.at(-1)?.[1]).toBe('yes\r');
t += 5_000;
waiting = false;
await b.onGatewayMessage(reply({ content: '2', attachments: [] }));
expect(sent.at(-1)?.[1]).toBe('2\r');
b.stop();
});
it('does not save a file for a message id that is not a snowflake, and says so', async () => {
const b = await make();
await b.onGatewayMessage(reply({ id: '../../x' }));
+14
View File
@@ -162,6 +162,20 @@ describe('POST /api/sessions/:id/discord-notify', () => {
bridge.stop();
});
it("does not reveal another user's session through its id prefix (multi-user)", async () => {
process.env.CODEMAN_MULTIUSER = '1';
const { app, bridge, ctx } = await harness({ username: 'bob', role: 'user' });
(ctx.sessions.get('test-session-1') as unknown as { owner: string }).owner = 'alice';
const res = await app.inject({
method: 'POST',
url: '/api/sessions/test-ses/discord-notify',
payload: { message: 'hi' },
});
expect(res.statusCode).toBe(404);
expect(res.json().error).not.toContain('test-session-1');
bridge.stop();
});
it('lets two different deliberate messages from one tab through within 3 s (neither is lost)', async () => {
const { app, bridge } = await harness();
await app.inject({ method: 'PUT', url: '/api/discord', payload: FULL });