Files
MailUI4Agents/plugins/pi-mail-bridge/test/workspace.test.mjs
JianFeeeee e6fd2fafdc feat: agent 邮件寻址能力全面补齐 + .new 别名替换
## 别名替换(让 .new 邮件可寻址)

repo/autoalias.go: AutoAliasFor + EnsureSessionAlias
- .new 建完会话立刻给别名(形如 dsh-重构导入路径)
- 名字与主题都要:只用主题跨 Agent 撞名,只用名字看不出聊什么
- sanitizeAliasPart 只留 unicode.IsLetter/IsDigit,其余折 -
- 撞名追加 -2/-3,全占用退 session-<uuid前8位>
- 不复用 SyncSessionAlias:那个假定已存在且跳过 manual
- 条件写入 WHERE alias IS NULL OR '',并发安全
- resolveTarget 的 .new 与默认会话两条路径都调

notifyRecipients 加三个字段(每个收件方拿到自己那个地址的版本):
- session_alias / reply_address / self_address
- 别名为空时退回省略 session 位,绝不写 new

FormatAddress(name,path,session) 空 path 也必须留 @ 与 .

## Agent 侧寻址发现(五个只读端点)

handler/agent_discovery.go:
- /agent/contacts + /agent/contacts/suggest(三段式补全)
- /agent/mail/{id} + /agent/mail/{id}/thread
- /agent/sessions/{id}/participants
- 不复用人类路由:scope 不同、审计需求不同
- 一律只读:归档/改名/权限决策仍只有人能做

repo/participants.go: SessionParticipants 逐封扫 from/to/cc
- Roles 用集合、MailCount 只数发信(0=还没开口的人)
- 发件人 path 不取 from_workspace(那列存的是 Agent 名)

repo.SuggestPaths 重写:mails.to_workspace(按 MAX(created_at) 倒序)
+ agents.workspaces 并集。原只读 workspaces,官方插件传 [] 永远空

## 共用模块(三插件逐字节相同)

lib/addressing.js: formatAddress/roleOf/replyAddressFor/selfAddressFor/participantsOfMail
lib/discovery.js: renderNameSuggestions/renderPathSuggestions/renderSessionSuggestions/
                  renderParticipants/renderContacts/renderThread

lib/inbox-format.js: renderMail 新增收件人/身份/可投递地址三段
  - selfName 参数(兼容旧调用不传的情况)

check-shared-libs.sh 纳入 addressing + discovery

## 插件侧

opencode: suggest_address + list_contacts + session_participants + read_thread + read_mail
dsh: 同上 + forward_mail(此前只有 opencode 有)+ upload_attachment 改真 multipart
pi: 同上(createMailTools 加 agentName 参数)

dsh: ctx.agents.create id collision 改为 readSession 探测后 resume
dsh: 关键路径日志改 console.error(ctx.logger 不进 journalctl)

## 测试

repo: autoalias_test.go 11 + participants_test.go 7 = 18 例
plugins: addressing.test 17 + discovery.test 23 + inbox-format.test 31 = 71 例
go test ./... + npm test(opencode 155 + dsh 173 + pi 199)全绿
端到端验证:admin 发 dsh@....new 抄送 opencode@....new
  → dsh 用 session_participants 取到地址 → send_mail 给 opencode
  → 地址取自工具返回值(.crisp-planet),未手工拼写
2026-09-03 12:09:12 +08:00

129 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 工作目录解析的回归测试。
*
* 这是「dsh 指定工作目录完全失效,所有对话都落在未分组下」那次故障的直接回归:
* 插件曾无视寻址里的 path 位,每封邮件自己拼一个 ~/.dsh/mail-sessions/mail-<uuid>
* 而 DSH 按 cwd 分组,于是所有邮件会话既不属于任何项目、彼此也不同组。
*
* node --test test/
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir, homedir } from 'node:os';
import { join } from 'node:path';
import { resolveWorkspaceCwd, ensureCwd, mailSessionFallback } from '../lib/workspace.js';
// 兜底目录现在由调用方给各平台不同。DSH 用 mailSessionFallback
// opencode 用插件启动时的 directory。
const fallbackOf = key => mailSessionFallback(key);
test('存在的绝对路径直接用作 cwd', () => {
const dir = mkdtempSync(join(tmpdir(), 'ws-test-'));
try {
const got = resolveWorkspaceCwd(dir, fallbackOf('mail-1'));
assert.equal(got.cwd, dir);
assert.equal(got.grouped, true);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('不变量:同一 path 的多封邮件得到同一个 cwd这才能同组', () => {
const dir = mkdtempSync(join(tmpdir(), 'ws-test-'));
try {
const a = resolveWorkspaceCwd(dir, fallbackOf('mail-aaa'));
const b = resolveWorkspaceCwd(dir, fallbackOf('mail-bbb'));
assert.equal(a.cwd, b.cwd, 'fallbackKey 不同却应得到同一个 cwd');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('path 为空时回退到兜底目录', () => {
const got = resolveWorkspaceCwd('', fallbackOf('mail-2'));
assert.equal(got.cwd, fallbackOf('mail-2'));
assert.equal(got.grouped, false);
});
test('path 缺失/非字符串时回退', () => {
for (const v of [undefined, null, 42, {}]) {
const got = resolveWorkspaceCwd(v, fallbackOf('mail-3'));
assert.equal(got.grouped, false);
assert.equal(got.cwd, fallbackOf('mail-3'));
}
});
test('不变量:不存在的目录不创建,回退到兜底', () => {
// 一个笔误(/home/porgram/x不该在磁盘上落下真目录 ——
// Agent 会在里面一无所获地干活,比明确回退更难排查。
const got = resolveWorkspaceCwd('/nonexistent/path/xyz-should-not-exist', fallbackOf('mail-4'));
assert.equal(got.grouped, false);
assert.equal(got.cwd, fallbackOf('mail-4'));
});
test('不变量:相对路径被拒绝', () => {
// cwd 的相对基准是 harness 进程的启动目录systemd 下通常是 /
// 那是个与邮件语义完全无关的量。
for (const rel of ['relative/path', './x', '../y', 'src']) {
const got = resolveWorkspaceCwd(rel, fallbackOf('mail-5'));
assert.equal(got.grouped, false, `${rel} 不该被当作工作目录`);
}
});
test('指向文件而非目录时回退', () => {
const dir = mkdtempSync(join(tmpdir(), 'ws-test-'));
const file = join(dir, 'a-file');
writeFileSync(file, 'x');
try {
const got = resolveWorkspaceCwd(file, fallbackOf('mail-6'));
assert.equal(got.grouped, false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('两端空白被修掉', () => {
const dir = mkdtempSync(join(tmpdir(), 'ws-test-'));
try {
const got = resolveWorkspaceCwd(` ${dir} `, fallbackOf('mail-7'));
assert.equal(got.cwd, dir);
assert.equal(got.grouped, true);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('ensureCwd 只建兜底目录,不碰寻址指定的目录', () => {
const base = mkdtempSync(join(tmpdir(), 'ws-ensure-'));
try {
const target = join(base, 'made-by-ensure');
ensureCwd(target, false);
// 建出来了
const got = resolveWorkspaceCwd(target, '');
assert.equal(got.grouped, true, 'ensureCwd 应已创建该目录');
// grouped=true 时不该创建(那种目录本来就存在)
const never = join(base, 'should-not-exist');
ensureCwd(never, true);
assert.equal(resolveWorkspaceCwd(never, '').grouped, false);
} finally {
rmSync(base, { recursive: true, force: true });
}
});
test('兜底为空串时返回空 cwd交给平台自己决定', () => {
// opencode 没配 directory 时就是这种情况session.create 不带 query.directory
// 由平台按自己的默认规则选目录。比硬塞一个我们猜的路径好。
const got = resolveWorkspaceCwd('', '');
assert.equal(got.cwd, '');
assert.equal(got.grouped, false);
});
test('mailSessionFallback 同一 key 稳定、不同 key 不同', () => {
assert.equal(mailSessionFallback('a'), mailSessionFallback('a'));
assert.notEqual(mailSessionFallback('a'), mailSessionFallback('b'));
assert.match(mailSessionFallback('a'), /mail-sessions/);
});