simlock alerts test checks a destination
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
Research direction
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.
Written by the indexing model from the issue text.
Description
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.
- Dominant language
- TypeScript
- Stars
- 20
- Forks
- 2
- Avg merge
- 8h 17m
- Merged PRs (30d)
- 273
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from callstackincubator/simlock
-
bug:needs-info
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
callstackincubator/simlock#657 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
callstackincubator/simlock#639 ·
Maintainers usually reply within 1 day
-
flaky-test
Difficulty 3/5 Half a day Newbie friendliness 58/100
callstackincubator/simlock#730 ·
Maintainers usually reply within 1 day
-
task:ready
Difficulty 4/5 3-5 days Newbie friendliness 50/100
callstackincubator/simlock#720 ·
Maintainers usually reply within 1 day
-
task:ready
Difficulty 5/5 Over a week Newbie friendliness 25/100
callstackincubator/simlock#719 · 1 comment ·
Maintainers usually reply within 1 day
All issues in callstackincubator/simlock
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
simkimsia/netlify-axi#19 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
subhranshudash13-dotcom/Contribo#74 ·
Maintainers usually reply within 1 day
-
enhancement frontend good first issue Stellar Wave trivial
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Godgift-care/sci-healthcare-frontend#166 · 1 comment ·
Maintainers usually reply within 1 day
-
backend good first issue security Stellar Wave trivial
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Godgift-care/sci-healthcare-backend#171 · 1 comment ·
Maintainers usually reply within 1 day