simlock alerts test checks a destination
维护者通常 1 天内回复
评估
- 难度
- 5/5
- 预计耗时
- 一周以上
- 新手友好度
- 35/100
调研方向
Start with the operation and dispatcher patterns in src/contract/operations.ts, src/gateway/dispatcher.ts, and src/daemon/dispatcher.ts, then trace request handling in src/daemon/server.ts and the admin client and CLI command patterns. Implement the specified alerts.test flow across the listed contract, daemon, client, CLI, and documentation files, including the protocol version bump. Done means the sample is sent once with the stated timeout and failures, worker behavior, CLI output, and tests match the specification.
由索引模型根据 Issue 内容生成。
描述
Part of #707.
Scope
simlock alerts test <destination> sends one sample message to one destination of the gateway, once, and says whether the receiver took it. After this PR, an operator finds a wrong URL or secret before an outage, not during one.
In short
The command asks the gateway to post one sample message to the named destination: one try, 10 s at most, counted from the POST's start, outside the destination's queue. It prints the receiver's HTTP status, or fails with the status or the error. A worker refuses it.
sequenceDiagram
participant C as simlock alerts test ops-slack
participant G as Gateway
participant R as Receiver
C->>G: alerts.test {destination: "ops-slack"} (admin)
G->>R: POST sample (one try, 10 s)
alt 2xx
R-->>G: 200
G-->>C: {destination, status: 200}
else other status, network error or timeout
G-->>C: ALERT_SEND_FAILED
end
Session, before and after:
$ simlock alerts test ops-slack
{"error":{"code":"USAGE","message":"Unknown command: alerts (run `simlock --help` for usage)"}} # before, exit 2
$ simlock alerts test ops-slack
Sent a test message to ops-slack: HTTP 200 # after, exit 0
$ simlock alerts test ops-slack --json
{"destination":"ops-slack","status":200}
$ simlock alerts test ops-slack
{"error":{"code":"ALERT_SEND_FAILED","message":"ops-slack answered HTTP 404"}} # exit 1
$ simlock alerts test ops-slack
{"error":{"code":"ALERT_SEND_FAILED","message":"ops-slack could not be reached: ECONNREFUSED"}} # exit 1
$ simlock alerts test ops-slack
{"error":{"code":"ALERT_SEND_FAILED","message":"ops-slack did not answer within 10 s"}} # exit 1
$ simlock alerts test nope
{"error":{"code":"UNKNOWN_ALERT_DESTINATION","message":"No alert destination named \"nope\". Check gateway.alerts.destinations in the gateway's config."}} # exit 12
$ simlock alerts test ops-slack # against a worker
{"error":{"code":"UNSUPPORTED_IN_WORKER_MODE","message":"alerts.test sends to a gateway's alert destinations, and this daemon is not a gateway; run it against the gateway"}} # exit 2
$ simlock alerts test
{"error":{"code":"USAGE","message":"Expected destination (run `simlock --help` for usage)"}} # exit 2
$ simlock alerts # and simlock alerts --help: usage on stdout, exit 0
Usage: simlock alerts test <destination> [--json]
$ simlock alerts foo
{"error":{"code":"USAGE","message":"Unknown alerts command: foo (run `simlock --help` for usage)"}} # exit 2
The sample message:
generic: the envelope{"id": "evt_<new id>", "seq": 0, "timestamp": <now, ms>, "event": "alerts.test-sent", "payload": {"destination": "<name>"}, "module": "alerts"}, with the headers and signature of everygenericmessage. It is not put on the bus and is not indocs/EVENTS.md;docs/CONFIGURATION.mdtells receivers it comes only fromsimlock alerts test.X-Simlock-Eventisalerts.test-sent.slack:{"text": "🔔 Test message from the simlock gateway to destination <name>."}.
If this goes wrong, the test says "sent" when the receiver refused it, or a working destination looks broken.
Technical spec
Facts
Commit ccb50353.
| # | Fact | Source |
|---|---|---|
| F1 | An operation is defineOperation({name, role, effect, input, output}) and listed in OPERATIONS; operations.test.ts has a row per operation with its role. |
src/contract/operations.ts:736-743, :1020-1055; src/contract/operations.test.ts:18-32 |
| F2 | The worker's Dispatcher answers gateway-only operations with unsupportedOnWorker(name, message), code UNSUPPORTED_IN_WORKER_MODE, details {operation}. |
src/daemon/dispatcher.ts:301-307, :1066-1073 |
| F3 | GatewayDispatcher is built in startGatewayDaemon from options main.ts passes. |
src/daemon/main.ts:844-866 |
| F4 | An error code is a row {code, kind, cliExitCode, httpStatus} in ERROR_TABLE with a details type in ErrorDetailsMap. UNKNOWN_WORKER is domain, exit 12, 404; WORKER_UNREACHABLE is transport, exit 1, 503. |
src/contract/errors.ts:20-114, :370-375 |
| F5 | The CLI prints a failure as {"error":{"code":…,"message":…}} on stderr and exits with the code's cliExitCode. |
src/cli/errors.ts:24; node dist/cli/main.js alerts test x printed {"error":{"code":"USAGE","message":"Unknown command: alerts (run \simlock --help` for usage)"}}`, exit 2 |
| F6 | A command group is a lazy CommandNode with children in COMMANDS; worker.ts shows subcommandOrUsage, connectDaemonClient, writeResult. |
src/cli/tree.ts:48-100; src/cli/commands/worker.ts:1-40 |
| F7 | The admin client lists each admin call (drainWorker → #call("worker.drain", input)), with input and output types in types.ts, and a rejecting stub for each. |
src/simlock-client/client.ts:254-259, :446, :671; src/simlock-client/types.ts:77-78 |
| F8 | startDaemon has an idGenerator option, default CryptoIdGenerator; event ids match ^evt_[A-Za-z0-9_-]{1,64}$. |
src/daemon/main.ts:131, :151; src/contract/schemas.ts:775 |
| F9 | docs/CLI.md has the exit-code table and one section per command group, e.g. ## simlock worker <list|drain|undrain|remove>; docs/CLIENT.md lists admin calls. |
docs/CLI.md:75-163, :1119; docs/CLIENT.md:853 |
| F12 | The socket serves an operation only if DaemonServer#handleRequest has a case for it; any other name answers UNKNOWN_REQUEST. gateway.dispatch(...) in tests skips that switch. |
src/daemon/server.ts:1136-1137, :1175-1176, :401 |
| F13 | A missing positional goes through requiredPositional: Expected <label> (run \simlock --help` for usage), exit 2; a group with no or --helpsubcommand prints its usage to stdout, exit 0; an unknown one isUnknown command: ` with the hint. |
src/cli/args.ts:24-27, :49-61; src/cli/commands/worker.ts:20-33; node dist/cli/main.js worker drain → {"error":{"code":"USAGE","message":"Expected worker-id (run \simlock --help` for usage)"}}` |
| F14 | Admin input and output types are re-exported from src/admin/index.ts; operations.test.ts has a role table and an effect matrix. |
src/admin/index.ts:74-75; src/contract/operations.test.ts:18-32, :175-180 |
| F15 | Gateway dispose runs before the server closes sockets; DAEMON_STOPPING gates only new calls. |
src/daemon/server.ts:406-409, :605-627 |
| F16 | GatewayDispatcher declares the shapes it needs from other modules itself (today e.g. GatewayTokenStore, which #712 may remove; the pattern stays); dependency-cruiser counts type-only imports. |
src/gateway/dispatcher.ts:84-92; .dependency-cruiser.mjs:263 |
| F10 | Every contract shape change bumps the protocol by one. | src/contract/AGENTS.md:59 |
| F11 | #719's createAlerts, WebhookPoster, generic request; #720's slack request. |
#719, #720 |
Modules touched
Searches: "worker.drain" (operation, dispatchers, client, HTTP), UNKNOWN_WORKER (errors, CLI docs), COMMANDS.
src/contract/operations.ts:alertsTest(alerts.test,admin,write, input{destination: string, 1–32 chars}, output{destination: string, status: number}).src/contract/errors.ts:UNKNOWN_ALERT_DESTINATION{destination};ALERT_SEND_FAILED{destination, status?: number, error?: string}:statusfor a non-2xx answer;errortimeout, a network code such asECONNREFUSED, ornetwork-error.src/contract/protocol.ts: bump by one.src/alerts/:Alerts.test(name)as under Surface;createAlertsoptions gainidGenerator: IdGenerator(for the sample's id);stop()also aborts a test try in flight. A test writes no log line of its own (the dispatchoperationline records the call), and a test aborted bystop()is not counted in "Alerts discarded at stop".test()afterstop()resolvesstoppedand posts nothing.src/gateway/dispatcher.ts:alerts.testhandler calling atestDestination(name)option (ADR 0035 §1), with the result shape declared in the gateway (F16), not imported fromsrc/alerts; without the option (no destinations) →UNKNOWN_ALERT_DESTINATION.src/daemon/dispatcher.ts:"alerts.test": unsupportedOnWorker("alerts.test", "alerts.test sends to a gateway's alert destinations, and this daemon is not a gateway; run it against the gateway").src/daemon/server.ts:case "alerts.test"in#handleRequest(F12).src/daemon/main.ts: passestestDestinationandidGeneratorthrough.src/simlock-client/:testAlertDestination(input), its types and stub;src/admin/index.tsre-exports the types (F14).src/contract/operations.test.ts: role table and effect matrix rows.PROTOCOL_VERSION_RANGEand every test that pins the current number (search the number itself and "protocol " insrc/, e.g.src/contract/protocol.test.ts,src/daemon/server.test.ts,src/daemon-protocol/index.test.ts,src/gateway/service.test.ts).src/cli/tree.ts,src/cli/commands/alerts.ts(new), and the root usage insrc/cli/index.ts:14-35gainsalerts test <destination>.- Docs:
docs/CLI.md(a section, two exit-code rows),docs/CLIENT.md(the call),docs/CONFIGURATION.md(point at the command),src/alerts/AGENTS.md.
flowchart LR
CLI["cli/commands/alerts.ts (new)"] --> CL["simlock-client *"]
CL --> CT["contract *"]
GD["gateway/dispatcher.ts *"] -. "testDestination(name)" .-> AL["alerts/ *"]
SRV["daemon/server.ts *"] --> GD
WD["daemon/dispatcher.ts *"]
main["daemon/main.ts *"] --> GD
main --> AL
Surface and extension points
Alerts gains test(destination: string): Promise<AlertTestResult>. It never rejects. AlertTestResult is {ok: true, status} or {ok: false, reason: "unknown-destination" | "status" | "unreachable" | "timeout" | "stopped", status?, error?}, error being the network code as in #719. The gateway dispatcher maps it: ok → output; unknown-destination → UNKNOWN_ALERT_DESTINATION; status, unreachable, timeout → ALERT_SEND_FAILED; stopped → DAEMON_STOPPING. No new extension point.
Contract and event changes
- New operation
alerts.test; new error codesUNKNOWN_ALERT_DESTINATION(domain, exit 12, HTTP 404) andALERT_SEND_FAILED(transport, exit 1, HTTP 502). This PR bumps the protocol by one,minwith it; the PR that merges second among the open tasks bumping this line (today #713, #714, #629-#632, #635-#637, #719, #720) bumps again. #713 also adds an operation toOPERATIONS, both dispatchers,server.ts, the client andcli/tree.ts: the second to merge takes the base branch and keeps both entries. - Input
destination: a string of 1 to 32 characters; any other →BAD_REQUEST. It is matched against configured names only, never used in a URL. - Messages:
No alert destination named "<destination>". Check gateway.alerts.destinations in the gateway's config.;<destination> answered HTTP <status>;<destination> could not be reached: <network code, e.g. ECONNREFUSED, else network-error>(never the error's message, which can hold the URL);<destination> did not answer within 10 s. - No event: the sample is not a fleet fact and is not put on the bus.
- No HTTP route (ADR 0035 §6).
Other code on the same state
Searches: the destination queue in src/alerts (#719), poster.post callers.
- The test does not use or wait for the destination's queue. A queued message may be in flight at the same time; the receiver sees both, in either order.
- The destination config is read-only after start (#719).
Failure modes
| Call | Outcome | What happens | Limit, counted from |
|---|---|---|---|
poster.post (sample) |
2xx | output {destination, status}; CLI exit 0 |
— |
poster.post |
other status (3xx not followed) | ALERT_SEND_FAILED "answered HTTP "; exit 1 |
— |
poster.post |
rejects | ALERT_SEND_FAILED "could not be reached: <network code, else network-error>", never the error's message; exit 1 |
— |
poster.post |
no answer | abort; ALERT_SEND_FAILED "did not answer within 10 s"; exit 1 |
10 s from the POST's start |
| lookup | unknown name, or no destinations | UNKNOWN_ALERT_DESTINATION; exit 12 |
— |
alerts.test on a worker |
— | UNSUPPORTED_IN_WORKER_MODE; exit 2 |
— |
| gateway stops during the test | — | Alerts.stop() aborts the test try and cancels its timer; test resolves stopped; the call answers DAEMON_STOPPING if its connection is still open (F15) |
none: stop waits for nothing |
- State: none written. Two tests at once each make their own POST.
Rules in play
- architecture, bounded cross-process waits: one try, 10 s.
- safety, "wire input is a claim":
destinationis only looked up by name. - ADR 0012, a worker answers fleet operations as a fleet of one: a gateway-only operation is refused with
UNSUPPORTED_IN_WORKER_MODE, asworker.drainis. - ADR 0035 §6.
Tests
Seam: the alerts.test operation through GatewayDispatcher with a real createAlerts and FakeWebhookPoster, in src/gateway/dispatcher.test.ts; the CLI in src/cli/index.test.ts; Alerts.test in src/alerts/alerts.test.ts.
alerts.test posts one sample to the named destination and answers its status(cases:genericwith the sample envelope, headers and a signature that verifies;slackwith the sample text). The sample is built by the destination's format definition, given an envelope of eventalerts.test-sent;slack's message table gains that one entry.alerts.test fails with ALERT_SEND_FAILED on a non-2xx status, a rejection and no answer within 10 s, and tries once(cases 404, 301, 503, rejection, 10 000 ms)alerts.test answers UNKNOWN_ALERT_DESTINATION for a name not configured and on a gateway with no destinationsstop during a test aborts its try, cancels its timer and answers DAEMON_STOPPING(alerts.test.tsforstoppedand no live timer;dispatcher.test.tsfor the mapping)a refused connection answers could not be reached with the network code, and neither the answer nor the failed call's operation log line holds the URLtest after stop resolves stopped and posts nothingthe sample is not put on the bus and does not go through the destination's queue(a queued message still in its retry wait is not delayed and not reordered)a worker answers alerts.test with UNSUPPORTED_IN_WORKER_MODE and its messagealerts.test needs the admin role and is a write(extends the role table and the effect matrix inoperations.test.ts)simlock alerts test prints the sent line, --json prints the output, a missing name prints the Expected line, simlock alerts and --help print the usage, and an unknown subcommand is a usage error(exit 0, 0, 2, 0, 2)simlock alerts test exits 1 on ALERT_SEND_FAILED and 12 on UNKNOWN_ALERT_DESTINATION with the error linethe admin client's testAlertDestination sends alerts.test and returns its output(src/simlock-client/client.test.ts, withScriptedConnectionas the existing cases)alerts.test over the gateway's real unix socket reaches a loopback receiver with the signed sample(extends #719's case ingateway-fleet.e2e.test.ts: the admin client connects to the gateway's socket, notgateway.dispatch, so a missingserver.tscase fails it; realFetchWebhookPoster)
Done when
- CI is green with the lines above.
- In
gateway-fleet.e2e.test.ts,alerts.testover the gateway's unix socket with agenericdestination on a loopback receiver answers status 200, and the receiver gets the signed sample. docs/CLI.mdhas the command, its output and both exit-code rows;docs/CLIENT.mdhastestAlertDestination.
Out of scope
- An HTTP route or a console button.
- Testing every destination at once.
Depends on
- #720
Written by an agent.
- 主要语言
- TypeScript
- 星标
- 20
- 派生
- 2
- 平均合并
- 8 小时
- 30 天内合并 PR
- 316
环境准备
这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
callstackincubator/simlock 的其他 Issue
-
bug:new
难度 2/5 1-3 小时 新手友好度 78/100
callstackincubator/simlock#817 ·
维护者通常 1 天内回复
-
bug:triage
难度 2/5 1-3 小时 新手友好度 76/100
callstackincubator/simlock#778 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 70/100
callstackincubator/simlock#639 · 1 条评论 ·
维护者通常 1 天内回复
-
task:draft
难度 4/5 3-5 天 新手友好度 35/100
callstackincubator/simlock#839 ·
维护者通常 1 天内回复
-
task:ready
难度 5/5 一周以上 新手友好度 35/100
callstackincubator/simlock#838 ·
维护者通常 1 天内回复
查看 callstackincubator/simlock 的全部 Issue
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 74/100
paperclipai/paperclip#17151 ·
维护者通常 1 天内回复
-
needs-triage
难度 2/5 1-3 小时 新手友好度 83/100
AgoraIO-Extensions/agent-infra#3159 ·
维护者通常 1 天内回复
-
blocklist addition
难度 1/5 1 小时以内 新手友好度 66/100
MetaMask/eth-phishing-detect#304596 ·
维护者通常 1 天内回复
-
friction
难度 2/5 1-3 小时 新手友好度 70/100
kentcdodds/kody#4142 ·
-
[Bug] Statistics | Task statistics are always empty on MikroORM with MySQL or SQLite可能已有人在做 @NdekoCode 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 76/100
ever-co/ever-gauzy#11214 ·
维护者通常 1 天内回复