208 lines
8.0 KiB
Python
208 lines
8.0 KiB
Python
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
SCRIPT = Path(__file__).parents[1] / "fixes" / "patch_openclaw_weixin_sender_identity.py"
|
|
SPEC = importlib.util.spec_from_file_location("sender_identity_patch", SCRIPT)
|
|
MODULE = importlib.util.module_from_spec(SPEC)
|
|
assert SPEC.loader is not None
|
|
SPEC.loader.exec_module(MODULE)
|
|
|
|
|
|
def plugin_fixture(tmp_path: Path) -> Path:
|
|
root = tmp_path / "plugin"
|
|
(root / "src/messaging").mkdir(parents=True)
|
|
(root / "dist/src/messaging").mkdir(parents=True)
|
|
(root / "src/monitor").mkdir(parents=True)
|
|
(root / "dist/src/monitor").mkdir(parents=True)
|
|
(root / "package.json").write_text(json.dumps({"name": "@tencent-weixin/openclaw-weixin"}), encoding="utf-8")
|
|
(root / "src/messaging/inbound.ts").write_text(
|
|
"const ctx = {\n From: from_user_id,\n To: from_user_id,\n};\n", encoding="utf-8"
|
|
)
|
|
(root / "dist/src/messaging/inbound.js").write_text(
|
|
"const ctx = {\n From: from_user_id,\n To: from_user_id,\n};\n", encoding="utf-8"
|
|
)
|
|
(root / "src/messaging/process-message.ts").write_text(
|
|
'''// AIOS Hermes image follow-up helpers end
|
|
|
|
export async function processOneMessage(
|
|
) {
|
|
await deps.channelRuntime.session.recordInboundSession({
|
|
storePath,
|
|
sessionKey: route.sessionKey,
|
|
ctx: finalized as Parameters<typeof deps.channelRuntime.session.recordInboundSession>[0]["ctx"],
|
|
updateLastRoute: {
|
|
sessionKey: route.mainSessionKey,
|
|
channel: "openclaw-weixin",
|
|
to: ctx.To,
|
|
accountId: deps.accountId,
|
|
},
|
|
onRecordError: (err) => deps.errLog(`recordInboundSession: ${String(err)}`),
|
|
});
|
|
try {
|
|
await deps.channelRuntime.reply.withReplyDispatcher({
|
|
dispatcher,
|
|
run: () =>
|
|
deps.channelRuntime.reply.dispatchReplyFromConfig({
|
|
ctx: finalized,
|
|
cfg: deps.config,
|
|
dispatcher,
|
|
replyOptions: {
|
|
...replyOptions,
|
|
...(replyProgressSender?.replyOptions ?? {}),
|
|
disableBlockStreaming: true,
|
|
},
|
|
}),
|
|
});
|
|
} catch (err) {}
|
|
}
|
|
''', encoding="utf-8"
|
|
)
|
|
(root / "dist/src/messaging/process-message.js").write_text(
|
|
'''// AIOS Hermes image follow-up helpers end
|
|
|
|
export async function processOneMessage(full, deps) {
|
|
await deps.channelRuntime.session.recordInboundSession({
|
|
storePath,
|
|
sessionKey: route.sessionKey,
|
|
ctx: finalized,
|
|
updateLastRoute: {
|
|
sessionKey: route.mainSessionKey,
|
|
channel: "openclaw-weixin",
|
|
to: ctx.To,
|
|
accountId: deps.accountId,
|
|
},
|
|
onRecordError: (err) => deps.errLog(`recordInboundSession: ${String(err)}`),
|
|
});
|
|
try {
|
|
await deps.channelRuntime.reply.withReplyDispatcher({
|
|
dispatcher,
|
|
run: () => deps.channelRuntime.reply.dispatchReplyFromConfig({
|
|
ctx: finalized,
|
|
cfg: deps.config,
|
|
dispatcher,
|
|
replyOptions: {
|
|
...replyOptions,
|
|
...(replyProgressSender?.replyOptions ?? {}),
|
|
disableBlockStreaming: true,
|
|
},
|
|
}),
|
|
});
|
|
}
|
|
catch (err) { }
|
|
}
|
|
''', encoding="utf-8"
|
|
)
|
|
(root / "src/monitor/monitor.ts").write_text(
|
|
'''import { processOneMessage } from "../messaging/process-message.js";
|
|
const RETRY_DELAY_MS = 2_000;
|
|
/**
|
|
* Long-poll loop: getUpdates -> normalize -> recordInboundSession -> dispatchReplyFromConfig.
|
|
*/
|
|
async function fixture() {
|
|
await processOneMessage(full, {
|
|
accountId,
|
|
config,
|
|
channelRuntime,
|
|
baseUrl,
|
|
cdnBaseUrl,
|
|
token,
|
|
typingTicket: cachedConfig.typingTicket,
|
|
log: opts.runtime?.log ?? (() => {}),
|
|
errLog,
|
|
});
|
|
}
|
|
''', encoding="utf-8"
|
|
)
|
|
(root / "dist/src/monitor/monitor.js").write_text(
|
|
'''import { processOneMessage } from "../messaging/process-message.js";
|
|
const RETRY_DELAY_MS = 2_000;
|
|
/**
|
|
* Long-poll loop: getUpdates -> normalize -> recordInboundSession -> dispatchReplyFromConfig.
|
|
*/
|
|
async function fixture() {
|
|
await processOneMessage(full, {
|
|
accountId,
|
|
config,
|
|
channelRuntime,
|
|
baseUrl,
|
|
cdnBaseUrl,
|
|
token,
|
|
typingTicket: cachedConfig.typingTicket,
|
|
log: opts.runtime?.log ?? (() => { }),
|
|
errLog,
|
|
});
|
|
}
|
|
''', encoding="utf-8"
|
|
)
|
|
return root
|
|
|
|
|
|
def test_check_is_non_mutating_and_reports_all_files(tmp_path: Path):
|
|
root = plugin_fixture(tmp_path)
|
|
result = MODULE.patch_plugin(root, apply=False)
|
|
assert result["status"] == "CHECKED"
|
|
assert result["changed"] is True
|
|
assert all(item["changed"] for item in result["files"].values())
|
|
assert "SenderId" not in (root / "src/messaging/inbound.ts").read_text(encoding="utf-8")
|
|
|
|
|
|
def test_apply_updates_source_and_dist_with_a_hashable_backup(tmp_path: Path):
|
|
root = plugin_fixture(tmp_path)
|
|
backup = tmp_path / "backup"
|
|
result = MODULE.patch_plugin(root, apply=True, backup_dir=backup)
|
|
assert result["status"] == "APPLIED"
|
|
for relative in MODULE.PATCHES:
|
|
assert result["files"][relative]["before_sha256"] == result["files"][relative]["backup_sha256"]
|
|
assert result["files"][relative]["after_sha256"] != result["files"][relative]["before_sha256"]
|
|
assert "SenderId: from_user_id" in (root / "src/messaging/inbound.ts").read_text(encoding="utf-8")
|
|
source_monitor = (root / "src/monitor/monitor.ts").read_text(encoding="utf-8")
|
|
dist_monitor = (root / "dist/src/monitor/monitor.js").read_text(encoding="utf-8")
|
|
assert "processOneMessageWithReplySessionRetry" in source_monitor
|
|
assert "processOneMessageWithReplySessionRetry" in dist_monitor
|
|
assert "resolveReplySessionInitRetryDelayMs" in source_monitor
|
|
assert "resolveReplySessionInitRetryDelayMs" in dist_monitor
|
|
assert "REPLY_SESSION_INIT_RETRY_DELAYS_MS" in source_monitor
|
|
assert "REPLY_SESSION_INIT_RETRY_DELAYS_MS" in dist_monitor
|
|
assert "}, abortSignal);" in source_monitor
|
|
assert "}, abortSignal);" in dist_monitor
|
|
source_process = (root / "src/messaging/process-message.ts").read_text(encoding="utf-8")
|
|
dist_process = (root / "dist/src/messaging/process-message.js").read_text(encoding="utf-8")
|
|
assert "dispatchWithSessionInitConflictRetry" in source_process
|
|
assert "dispatchWithSessionInitConflictRetry" in dist_process
|
|
assert "resolveDispatchSessionInitRetryDelayMs" in source_process
|
|
assert "resolveDispatchSessionInitRetryDelayMs" in dist_process
|
|
assert "const sessionMetaTasks: Promise<unknown>[] = [];" in source_process
|
|
assert "trackSessionMetaTask: (task) => sessionMetaTasks.push(task)" in source_process
|
|
assert "await Promise.all(sessionMetaTasks);" in source_process
|
|
assert "const sessionMetaTasks = [];" in dist_process
|
|
assert "trackSessionMetaTask: (task) => sessionMetaTasks.push(task)" in dist_process
|
|
assert "await Promise.all(sessionMetaTasks);" in dist_process
|
|
|
|
|
|
def test_second_check_is_idempotent(tmp_path: Path):
|
|
root = plugin_fixture(tmp_path)
|
|
MODULE.patch_plugin(root, apply=True, backup_dir=tmp_path / "backup")
|
|
result = MODULE.patch_plugin(root, apply=False)
|
|
assert result["changed"] is False
|
|
|
|
|
|
def test_shape_drift_fails_closed(tmp_path: Path):
|
|
root = plugin_fixture(tmp_path)
|
|
(root / "src/messaging/inbound.ts").write_text("unexpected", encoding="utf-8")
|
|
with pytest.raises(RuntimeError, match="target_shape_mismatch"):
|
|
MODULE.patch_plugin(root, apply=False)
|
|
|
|
|
|
def test_apply_requires_new_backup_directory(tmp_path: Path):
|
|
root = plugin_fixture(tmp_path)
|
|
with pytest.raises(RuntimeError, match="backup_dir_required"):
|
|
MODULE.patch_plugin(root, apply=True)
|
|
existing = tmp_path / "existing"
|
|
existing.mkdir()
|
|
with pytest.raises(FileExistsError):
|
|
MODULE.patch_plugin(root, apply=True, backup_dir=existing)
|