[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71Q8QN4K8T11JP3C6YGDYHS":3,"skill-same:01M71Q8QN4K8T11JP3C6YGDYHS":64},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":37,"linkOnly":38,"markdownContent":39,"markdownBody":40,"frontMatter":41,"refName":42,"commitSha":43,"skillDirectory":44,"forksCount":45,"repositoryHtmlUrl":46,"repositoryHomepageUrl":6,"ownerHtmlUrl":47,"ownerBlogUrl":48,"ownerDescription":6,"updatedAt":49,"install":50,"resources":63},"01M71Q8QN4K8T11JP3C6YGDYHS","gleam-testing",null,"Best practices for writing Gleam tests with gleeunit. Use when writing or reviewing Gleam test files, fixing silently-passing tests, or choosing assertion patterns.","kasuboski\u002Fpig","kasuboski","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F2403190?v=4","01M71Q8M5P8MJCB3TZTMMG73S6","Josh Kasuboski","01M71Q8M6V9HNR6MJN75VQZDB7",4,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fkasuboski\u002Fpig\u002Fblob\u002Fmain\u002F.pi\u002Fskills\u002Fgleam-testing\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36],"testing","gleam testing","gleam","files","writing","when","patterns","for","tests","use","best practices","with","test","2026-11-10 13:02:02",false,"---\nname: gleam-testing\ndescription: Best practices for writing Gleam tests with gleeunit. Use when writing or reviewing Gleam test files, fixing silently-passing tests, or choosing assertion patterns.\n---\n\n# Gleam Testing Best Practices\n\n## Critical: gleeunit Only Fails on Panics\n\nGleeunit tests pass if the function completes without panicking. **Returning `False` does NOT fail the test.** This is the #1 source of silently-passing bogus tests.\n\n```gleam\n\u002F\u002F ❌ WRONG — returns False, test silently passes\npub fn my_test() {\n  value == \"expected\"\n}\n\n\u002F\u002F ✅ CORRECT — panics on mismatch\npub fn my_test() {\n  assert value == \"expected\"\n}\n```\n\nThe same applies to boolean expressions as the last line of any test. A bare `count == 4` evaluates and discards. Use assertions.\n\n## Two Assertion Constructs — Do Not Confuse Them\n\nGleam has **two** distinct assertion mechanisms. They look similar but serve different purposes:\n\n### `assert \u003Cbool_expr>` — Boolean Assertion\n\nEvaluates the expression. Panics if it's `False`, continues if it's `True`.\n\n```gleam\nassert value == \"expected\"\nassert count == 4\nassert mode == \"wal\" || mode == \"memory\"\nassert string.contains(output, \"hello\")\nassert !string.contains(output, \"config\")\nassert keys == [\"alpha\", \"beta\"]\n```\n\n### `let assert \u003Cpattern> = \u003Cexpr>` — Assertive Pattern Match\n\nEvaluates the expression, then pattern-matches against the pattern. Panics if the pattern doesn't match. Binds variables from the pattern.\n\n```gleam\nlet assert Ok(value) = result           \u002F\u002F unwraps Ok, panics on Error\nlet assert Error(e) = result            \u002F\u002F unwraps Error, panics on Ok\nlet assert Some(value) = option_value   \u002F\u002F unwraps Some, panics on None\nlet assert None = maybe_thing           \u002F\u002F panics if Some\nlet assert [item] = list_with_one       \u002F\u002F panics unless exactly one element\nlet assert Ok(Nil) = some_operation()   \u002F\u002F asserts Ok(Nil) specifically\n```\n\n## Assertion Patterns — When to Use What\n\n| Situation | Use |\n|---|---|\n| Check exact equality | `assert actual == expected` |\n| Check inequality | `assert actual != expected` |\n| Compound boolean (OR, AND) | `assert a \\|\\| b` |\n| Assert string\u002Flist contains | `assert string.contains(s, \"x\")` |\n| Assert Result is Ok AND use the value | `let assert Ok(val) = result` |\n| Assert Result is Ok, don't need value | `let assert Ok(Nil) = result` |\n| Assert Result is Error | `let assert Error(e) = result` |\n| Assert specific error variant | `let assert Error(NotFound(key: k)) = ...` |\n| Assert Option is Some | `let assert Some(v) = option` |\n| Assert Option is None | `let assert None = option` |\n| Assert list has exactly one element | `let assert [item] = list` |\n\n## Common Pitfalls\n\n### Bare boolean as last expression\n```gleam\n\u002F\u002F ❌ Silent pass on False\npub fn test_something() {\n  let assert Ok(value) = compute()\n  value == \"expected\"  \u002F\u002F \u003C-- returned, not asserted\n}\n\n\u002F\u002F ✅ Fix\npub fn test_something() {\n  let assert Ok(value) = compute()\n  assert value == \"expected\"\n}\n```\n\n### Weak vs strong assertions on Results\n```gleam\n\u002F\u002F Weak — only checks it's Ok, not the value\nlet assert Ok(_) = result\n\n\u002F\u002F Stronger — checks the exact value\nlet assert Ok(\"expected\") = result\n```\n\n### Chaining assertions\n```gleam\nlet assert Ok(content) = read(conn, \"\u002Ffile.txt\")\nassert content == \"hello\"\n```\n\n### assert inside case expressions\n```gleam\n\u002F\u002F ❌ WRONG — `assert` is a statement, not a case branch expression\ncase msg {\n  message.Assistant(content:, ..) ->\n    assert string.contains(content, \"hello\")\n  _ -> panic as \"expected Assistant\"\n}\n\n\u002F\u002F ✅ Fix — use a block\ncase msg {\n  message.Assistant(content:, ..) -> {\n    assert string.contains(content, \"hello\")\n    Nil\n  }\n  _ -> panic as \"expected Assistant\"\n}\n\n\u002F\u002F ✅ Or better — destructure first, then assert\nlet assert message.Assistant(content:, ..) = msg\nassert string.contains(content, \"hello\")\n```\n\n## Test File Structure\n\n```gleam\nimport gleeunit\n\npub fn main() -> Nil {\n  gleeunit.main()\n}\n\nfn with_db(f: fn(sqlight.Connection) -> a) -> a {\n  let assert Ok(conn) = sqlight.open(\":memory:\")\n  let assert Ok(Nil) = schema.init(conn)\n  let result = f(conn)\n  let assert Ok(Nil) = sqlight.close(conn)\n  result\n}\n\npub fn my_feature_test() {\n  with_db(fn(conn) {\n    let assert Ok(Nil) = write(conn, \"\u002Ffile.txt\", \"hello\")\n    let assert Ok(content) = read(conn, \"\u002Ffile.txt\")\n    assert content == \"hello\"\n  })\n}\n```\n\n## Testing OTP Actors and Processes\n\n### The Golden Rule: Never Use `process.sleep`\n\n**Never use `process.sleep` in tests.** It is brittle, slow, and flaky:\n\n- **Race conditions**: Sleep durations are arbitrary guesses. Too short = flakes. Too long = slow suite.\n- **Fragile**: System load, scheduler behavior, or actor changes break your timing assumptions.\n- **Unnecessary**: Gleam\u002FOTP gives you `process.receive` and `Subject` for deterministic sync.\n\nInstead, use one of the synchronization patterns below.\n\n### Pattern 1: The `send_and_confirm` Synchronization Pattern\n\nThe fundamental building block for testing any actor that fans out or forwards messages:\n\n1. **Start the actor** and get its `Subject`\n2. **Register a test `Subject`** as a consumer\u002Fsubscriber via the actor's public API\n3. **Send a message** to the actor through its public API\n4. **`process.receive` on the test subject** to block until the actor finishes processing\n5. **Now assert** on side effects or the received message itself\n\n```gleam\nimport gleam\u002Ferlang\u002Fprocess\n\n\u002F\u002F Generic helper — adapt the types to your actor\nfn send_and_confirm(\n  actor: process.Subject(actor_message),\n  msg: actor_message,\n  test_consumer: process.Subject(output),\n) -> output {\n  process.send(actor, msg)\n  let assert Ok(received) = process.receive(test_consumer, 2000)\n  received\n}\n\npub fn my_actor_test() {\n  let assert Ok(actor) = my_actor.start()\n  let consumer = process.new_subject()\n  process.send(actor, my_actor.Subscribe(consumer))\n\n  let received = send_and_confirm(actor, my_actor.DoSomething(\"hello\"), consumer)\n  assert received == expected\n\n  process.send(actor, my_actor.Stop)\n}\n```\n\n**Why this works:** If the actor processes the message (including any side effects) *before* forwarding to consumers, then receiving on the consumer guarantees all side effects completed. No race, no sleep.\n\n### Pattern 2: The Test Listener (Instrumented Side-Effect Capture)\n\nFor actors that produce side effects you cannot observe via consumer subscription (telemetry, logging, metrics), use a **test listener**: a lightweight observer that attaches through the public interface and captures outputs for assertion.\n\nThe test listener does NOT mock or replace the actor. The actor runs its real code path. The listener is purely an observation tool.\n\n\n**Steps:**\n1. **Attach** the listener before starting the actor\n2. **Start the actor** and register a test consumer\n3. **Send a message** and confirm via `send_and_confirm`\n4. **Query the listener** for captured side effects\n5. **Assert** on the captured data\n6. **Detach** the listener in cleanup\n\n```gleam\npub fn test_actor_produces_side_effect() {\n  let listener = my_listener.attach()           \u002F\u002F 1. attach observer\n  let assert Ok(actor) = my_actor.start()       \u002F\u002F 2. start actor\n  let consumer = process.new_subject()\n  process.send(actor, my_actor.Subscribe(consumer))\n\n  send_and_confirm(actor, my_actor.DoX(), consumer)  \u002F\u002F 3. exercise\n\n  let captured = my_listener.get_events(listener)     \u002F\u002F 4. query\n  let assert [XHappened(detail:)] = captured         \u002F\u002F 5. assert\n  assert detail == \"expected\"\n\n  my_listener.detach(listener)                       \u002F\u002F 6. cleanup\n  process.send(actor, my_actor.Stop)\n}\n```\n\n**Negative assertions** work the same way — confirm the message was processed, then assert the listener captured nothing:\n\n```gleam\n  send_and_confirm(actor, my_actor.DoY(), consumer)\n  let assert [] = my_listener.get_events(listener)\n```\n\n### Pattern 3: Setup\u002FCleanup Helpers\n\nCombine actor start + consumer registration + listener attachment into reusable helpers. Return a tuple of `(handles, cleanup_fn)`:\n\n```gleam\nfn setup() {\n  let assert Ok(actor) = my_actor.start()\n  let consumer = process.new_subject()\n  process.send(actor, my_actor.Subscribe(consumer))\n  #(\n    #(actor, consumer),\n    fn() { process.send(actor, my_actor.Stop) },\n  )\n}\n\nfn setup_with_listener() {\n  let listener = my_listener.attach()\n  let assert Ok(actor) = my_actor.start()\n  let consumer = process.new_subject()\n  process.send(actor, my_actor.Subscribe(consumer))\n  #(\n    #(actor, consumer, listener),\n    fn() {\n      my_listener.detach(listener)\n      process.send(actor, my_actor.Stop)\n    },\n  )\n}\n\npub fn test_with_helper() {\n  let #(#(actor, consumer, listener), cleanup) = setup_with_listener()\n  \u002F\u002F ... test code ...\n  cleanup()\n}\n```\n\n### Concrete Example: Dispatcher Actor Tests\n\nHere is how the patterns above look in practice, from `test\u002Fpig\u002Fobs\u002Fdispatcher_test.gleam`. The dispatcher is an actor that receives `SessionEvent`s, emits `:telemetry` as a side effect, then fans out to registered consumers.\n\n```gleam\n\u002F\u002F The send_and_confirm helper — sends event, blocks until consumer confirms receipt\nfn send_and_confirm(\n  disp: process.Subject(dispatcher.DispatcherMessage),\n  event: events.SessionEvent,\n  consumer: process.Subject(events.SessionEvent),\n) -> events.SessionEvent {\n  process.send(disp, dispatcher.Event(event))\n  let assert Ok(received) = process.receive(consumer, 2000)\n  received\n}\n\n\u002F\u002F Setup helper with a :telemetry listener attached (Pattern 3)\nfn setup_with_listener() {\n  let handle = listener.attach()           \u002F\u002F attach telemetry capture\n  let assert Ok(disp) = dispatcher.start()\n  let consumer = process.new_subject()\n  process.send(disp, dispatcher.RegisterConsumer(consumer))\n  #(\n    #(disp, consumer, handle),\n    fn() {\n      listener.detach(handle)\n      process.send(disp, dispatcher.Stop)\n    },\n  )\n}\n\n\u002F\u002F Test: positive assertion — event produces the right telemetry (Pattern 2)\npub fn dispatcher_emits_inference_start_telemetry_test() {\n  let #(#(disp, consumer, handle), cleanup) = setup_with_listener()\n\n  let event = InferenceStarted(model: \"gpt-4\", message_count: 3)\n  send_and_confirm(disp, event, consumer)\n\n  let captured = listener.get_events(handle)\n  let assert [InferenceStart(model:, message_count:)] = captured\n  assert model == \"gpt-4\"\n  assert message_count == 3\n\n  cleanup()\n}\n\n\u002F\u002F Test: negative assertion — event does NOT produce telemetry (Pattern 2)\npub fn dispatcher_does_not_emit_telemetry_for_session_ended_test() {\n  let #(#(disp, consumer, handle), cleanup) = setup_with_listener()\n\n  send_and_confirm(disp, SessionEnded(reason: NormalEnd), consumer)\n  let assert [] = listener.get_events(handle)\n\n  cleanup()\n}\n```\n\n### Testing Resilience (Dead Consumers, Dynamic Registration)\n\nThese are specializations of Pattern 1, using `send_and_confirm` to prove the actor is still alive after adverse conditions:\n\n**Dead consumer resilience** — register an abandoned subject, send an event (actor must not crash), then prove it's alive by registering a live consumer and sending again:\n\n```gleam\npub fn test_dead_consumer_does_not_crash_actor() {\n  let assert Ok(actor) = my_actor.start()\n  let dead = process.new_subject()\n  process.send(actor, my_actor.Subscribe(dead))\n  process.send(actor, my_actor.DoSomething())  \u002F\u002F must not crash\n\n  \u002F\u002F Prove actor is still alive with a new consumer\n  let live = process.new_subject()\n  process.send(actor, my_actor.Subscribe(live))\n  let received = send_and_confirm(actor, my_actor.DoSomethingElse(), live)\n  \u002F\u002F assert on received...\n\n  process.send(actor, my_actor.Stop)\n}\n```\n\n**Dynamic registration** — send before any consumer exists (nobody receives), then register and confirm the next message arrives:\n\n```gleam\npub fn test_dynamic_registration() {\n  let assert Ok(actor) = my_actor.start()\n  process.send(actor, my_actor.Event(first))  \u002F\u002F no consumer yet\n\n  let consumer = process.new_subject()\n  process.send(actor, my_actor.Subscribe(consumer))\n\n  let received = send_and_confirm(actor, my_actor.Event(second), consumer)\n  assert received == second\n\n  process.send(actor, my_actor.Stop)\n}\n```\n","# Gleam Testing Best Practices\n\n## Critical: gleeunit Only Fails on Panics\n\nGleeunit tests pass if the function completes without panicking. **Returning `False` does NOT fail the test.** This is the #1 source of silently-passing bogus tests.\n\n```gleam\n\u002F\u002F ❌ WRONG — returns False, test silently passes\npub fn my_test() {\n  value == \"expected\"\n}\n\n\u002F\u002F ✅ CORRECT — panics on mismatch\npub fn my_test() {\n  assert value == \"expected\"\n}\n```\n\nThe same applies to boolean expressions as the last line of any test. A bare `count == 4` evaluates and discards. Use assertions.\n\n## Two Assertion Constructs — Do Not Confuse Them\n\nGleam has **two** distinct assertion mechanisms. They look similar but serve different purposes:\n\n### `assert \u003Cbool_expr>` — Boolean Assertion\n\nEvaluates the expression. Panics if it's `False`, continues if it's `True`.\n\n```gleam\nassert value == \"expected\"\nassert count == 4\nassert mode == \"wal\" || mode == \"memory\"\nassert string.contains(output, \"hello\")\nassert !string.contains(output, \"config\")\nassert keys == [\"alpha\", \"beta\"]\n```\n\n### `let assert \u003Cpattern> = \u003Cexpr>` — Assertive Pattern Match\n\nEvaluates the expression, then pattern-matches against the pattern. Panics if the pattern doesn't match. Binds variables from the pattern.\n\n```gleam\nlet assert Ok(value) = result           \u002F\u002F unwraps Ok, panics on Error\nlet assert Error(e) = result            \u002F\u002F unwraps Error, panics on Ok\nlet assert Some(value) = option_value   \u002F\u002F unwraps Some, panics on None\nlet assert None = maybe_thing           \u002F\u002F panics if Some\nlet assert [item] = list_with_one       \u002F\u002F panics unless exactly one element\nlet assert Ok(Nil) = some_operation()   \u002F\u002F asserts Ok(Nil) specifically\n```\n\n## Assertion Patterns — When to Use What\n\n| Situation | Use |\n|---|---|\n| Check exact equality | `assert actual == expected` |\n| Check inequality | `assert actual != expected` |\n| Compound boolean (OR, AND) | `assert a \\|\\| b` |\n| Assert string\u002Flist contains | `assert string.contains(s, \"x\")` |\n| Assert Result is Ok AND use the value | `let assert Ok(val) = result` |\n| Assert Result is Ok, don't need value | `let assert Ok(Nil) = result` |\n| Assert Result is Error | `let assert Error(e) = result` |\n| Assert specific error variant | `let assert Error(NotFound(key: k)) = ...` |\n| Assert Option is Some | `let assert Some(v) = option` |\n| Assert Option is None | `let assert None = option` |\n| Assert list has exactly one element | `let assert [item] = list` |\n\n## Common Pitfalls\n\n### Bare boolean as last expression\n```gleam\n\u002F\u002F ❌ Silent pass on False\npub fn test_something() {\n  let assert Ok(value) = compute()\n  value == \"expected\"  \u002F\u002F \u003C-- returned, not asserted\n}\n\n\u002F\u002F ✅ Fix\npub fn test_something() {\n  let assert Ok(value) = compute()\n  assert value == \"expected\"\n}\n```\n\n### Weak vs strong assertions on Results\n```gleam\n\u002F\u002F Weak — only checks it's Ok, not the value\nlet assert Ok(_) = result\n\n\u002F\u002F Stronger — checks the exact value\nlet assert Ok(\"expected\") = result\n```\n\n### Chaining assertions\n```gleam\nlet assert Ok(content) = read(conn, \"\u002Ffile.txt\")\nassert content == \"hello\"\n```\n\n### assert inside case expressions\n```gleam\n\u002F\u002F ❌ WRONG — `assert` is a statement, not a case branch expression\ncase msg {\n  message.Assistant(content:, ..) ->\n    assert string.contains(content, \"hello\")\n  _ -> panic as \"expected Assistant\"\n}\n\n\u002F\u002F ✅ Fix — use a block\ncase msg {\n  message.Assistant(content:, ..) -> {\n    assert string.contains(content, \"hello\")\n    Nil\n  }\n  _ -> panic as \"expected Assistant\"\n}\n\n\u002F\u002F ✅ Or better — destructure first, then assert\nlet assert message.Assistant(content:, ..) = msg\nassert string.contains(content, \"hello\")\n```\n\n## Test File Structure\n\n```gleam\nimport gleeunit\n\npub fn main() -> Nil {\n  gleeunit.main()\n}\n\nfn with_db(f: fn(sqlight.Connection) -> a) -> a {\n  let assert Ok(conn) = sqlight.open(\":memory:\")\n  let assert Ok(Nil) = schema.init(conn)\n  let result = f(conn)\n  let assert Ok(Nil) = sqlight.close(conn)\n  result\n}\n\npub fn my_feature_test() {\n  with_db(fn(conn) {\n    let assert Ok(Nil) = write(conn, \"\u002Ffile.txt\", \"hello\")\n    let assert Ok(content) = read(conn, \"\u002Ffile.txt\")\n    assert content == \"hello\"\n  })\n}\n```\n\n## Testing OTP Actors and Processes\n\n### The Golden Rule: Never Use `process.sleep`\n\n**Never use `process.sleep` in tests.** It is brittle, slow, and flaky:\n\n- **Race conditions**: Sleep durations are arbitrary guesses. Too short = flakes. Too long = slow suite.\n- **Fragile**: System load, scheduler behavior, or actor changes break your timing assumptions.\n- **Unnecessary**: Gleam\u002FOTP gives you `process.receive` and `Subject` for deterministic sync.\n\nInstead, use one of the synchronization patterns below.\n\n### Pattern 1: The `send_and_confirm` Synchronization Pattern\n\nThe fundamental building block for testing any actor that fans out or forwards messages:\n\n1. **Start the actor** and get its `Subject`\n2. **Register a test `Subject`** as a consumer\u002Fsubscriber via the actor's public API\n3. **Send a message** to the actor through its public API\n4. **`process.receive` on the test subject** to block until the actor finishes processing\n5. **Now assert** on side effects or the received message itself\n\n```gleam\nimport gleam\u002Ferlang\u002Fprocess\n\n\u002F\u002F Generic helper — adapt the types to your actor\nfn send_and_confirm(\n  actor: process.Subject(actor_message),\n  msg: actor_message,\n  test_consumer: process.Subject(output),\n) -> output {\n  process.send(actor, msg)\n  let assert Ok(received) = process.receive(test_consumer, 2000)\n  received\n}\n\npub fn my_actor_test() {\n  let assert Ok(actor) = my_actor.start()\n  let consumer = process.new_subject()\n  process.send(actor, my_actor.Subscribe(consumer))\n\n  let received = send_and_confirm(actor, my_actor.DoSomething(\"hello\"), consumer)\n  assert received == expected\n\n  process.send(actor, my_actor.Stop)\n}\n```\n\n**Why this works:** If the actor processes the message (including any side effects) *before* forwarding to consumers, then receiving on the consumer guarantees all side effects completed. No race, no sleep.\n\n### Pattern 2: The Test Listener (Instrumented Side-Effect Capture)\n\nFor actors that produce side effects you cannot observe via consumer subscription (telemetry, logging, metrics), use a **test listener**: a lightweight observer that attaches through the public interface and captures outputs for assertion.\n\nThe test listener does NOT mock or replace the actor. The actor runs its real code path. The listener is purely an observation tool.\n\n\n**Steps:**\n1. **Attach** the listener before starting the actor\n2. **Start the actor** and register a test consumer\n3. **Send a message** and confirm via `send_and_confirm`\n4. **Query the listener** for captured side effects\n5. **Assert** on the captured data\n6. **Detach** the listener in cleanup\n\n```gleam\npub fn test_actor_produces_side_effect() {\n  let listener = my_listener.attach()           \u002F\u002F 1. attach observer\n  let assert Ok(actor) = my_actor.start()       \u002F\u002F 2. start actor\n  let consumer = process.new_subject()\n  process.send(actor, my_actor.Subscribe(consumer))\n\n  send_and_confirm(actor, my_actor.DoX(), consumer)  \u002F\u002F 3. exercise\n\n  let captured = my_listener.get_events(listener)     \u002F\u002F 4. query\n  let assert [XHappened(detail:)] = captured         \u002F\u002F 5. assert\n  assert detail == \"expected\"\n\n  my_listener.detach(listener)                       \u002F\u002F 6. cleanup\n  process.send(actor, my_actor.Stop)\n}\n```\n\n**Negative assertions** work the same way — confirm the message was processed, then assert the listener captured nothing:\n\n```gleam\n  send_and_confirm(actor, my_actor.DoY(), consumer)\n  let assert [] = my_listener.get_events(listener)\n```\n\n### Pattern 3: Setup\u002FCleanup Helpers\n\nCombine actor start + consumer registration + listener attachment into reusable helpers. Return a tuple of `(handles, cleanup_fn)`:\n\n```gleam\nfn setup() {\n  let assert Ok(actor) = my_actor.start()\n  let consumer = process.new_subject()\n  process.send(actor, my_actor.Subscribe(consumer))\n  #(\n    #(actor, consumer),\n    fn() { process.send(actor, my_actor.Stop) },\n  )\n}\n\nfn setup_with_listener() {\n  let listener = my_listener.attach()\n  let assert Ok(actor) = my_actor.start()\n  let consumer = process.new_subject()\n  process.send(actor, my_actor.Subscribe(consumer))\n  #(\n    #(actor, consumer, listener),\n    fn() {\n      my_listener.detach(listener)\n      process.send(actor, my_actor.Stop)\n    },\n  )\n}\n\npub fn test_with_helper() {\n  let #(#(actor, consumer, listener), cleanup) = setup_with_listener()\n  \u002F\u002F ... test code ...\n  cleanup()\n}\n```\n\n### Concrete Example: Dispatcher Actor Tests\n\nHere is how the patterns above look in practice, from `test\u002Fpig\u002Fobs\u002Fdispatcher_test.gleam`. The dispatcher is an actor that receives `SessionEvent`s, emits `:telemetry` as a side effect, then fans out to registered consumers.\n\n```gleam\n\u002F\u002F The send_and_confirm helper — sends event, blocks until consumer confirms receipt\nfn send_and_confirm(\n  disp: process.Subject(dispatcher.DispatcherMessage),\n  event: events.SessionEvent,\n  consumer: process.Subject(events.SessionEvent),\n) -> events.SessionEvent {\n  process.send(disp, dispatcher.Event(event))\n  let assert Ok(received) = process.receive(consumer, 2000)\n  received\n}\n\n\u002F\u002F Setup helper with a :telemetry listener attached (Pattern 3)\nfn setup_with_listener() {\n  let handle = listener.attach()           \u002F\u002F attach telemetry capture\n  let assert Ok(disp) = dispatcher.start()\n  let consumer = process.new_subject()\n  process.send(disp, dispatcher.RegisterConsumer(consumer))\n  #(\n    #(disp, consumer, handle),\n    fn() {\n      listener.detach(handle)\n      process.send(disp, dispatcher.Stop)\n    },\n  )\n}\n\n\u002F\u002F Test: positive assertion — event produces the right telemetry (Pattern 2)\npub fn dispatcher_emits_inference_start_telemetry_test() {\n  let #(#(disp, consumer, handle), cleanup) = setup_with_listener()\n\n  let event = InferenceStarted(model: \"gpt-4\", message_count: 3)\n  send_and_confirm(disp, event, consumer)\n\n  let captured = listener.get_events(handle)\n  let assert [InferenceStart(model:, message_count:)] = captured\n  assert model == \"gpt-4\"\n  assert message_count == 3\n\n  cleanup()\n}\n\n\u002F\u002F Test: negative assertion — event does NOT produce telemetry (Pattern 2)\npub fn dispatcher_does_not_emit_telemetry_for_session_ended_test() {\n  let #(#(disp, consumer, handle), cleanup) = setup_with_listener()\n\n  send_and_confirm(disp, SessionEnded(reason: NormalEnd), consumer)\n  let assert [] = listener.get_events(handle)\n\n  cleanup()\n}\n```\n\n### Testing Resilience (Dead Consumers, Dynamic Registration)\n\nThese are specializations of Pattern 1, using `send_and_confirm` to prove the actor is still alive after adverse conditions:\n\n**Dead consumer resilience** — register an abandoned subject, send an event (actor must not crash), then prove it's alive by registering a live consumer and sending again:\n\n```gleam\npub fn test_dead_consumer_does_not_crash_actor() {\n  let assert Ok(actor) = my_actor.start()\n  let dead = process.new_subject()\n  process.send(actor, my_actor.Subscribe(dead))\n  process.send(actor, my_actor.DoSomething())  \u002F\u002F must not crash\n\n  \u002F\u002F Prove actor is still alive with a new consumer\n  let live = process.new_subject()\n  process.send(actor, my_actor.Subscribe(live))\n  let received = send_and_confirm(actor, my_actor.DoSomethingElse(), live)\n  \u002F\u002F assert on received...\n\n  process.send(actor, my_actor.Stop)\n}\n```\n\n**Dynamic registration** — send before any consumer exists (nobody receives), then register and confirm the next message arrives:\n\n```gleam\npub fn test_dynamic_registration() {\n  let assert Ok(actor) = my_actor.start()\n  process.send(actor, my_actor.Event(first))  \u002F\u002F no consumer yet\n\n  let consumer = process.new_subject()\n  process.send(actor, my_actor.Subscribe(consumer))\n\n  let received = send_and_confirm(actor, my_actor.Event(second), consumer)\n  assert received == second\n\n  process.send(actor, my_actor.Stop)\n}\n```\n",{"name":5,"description":7},"main","7c719c27d92c856aab8e38e19b2fc9bfe1877e0e",".pi\u002Fskills\u002Fgleam-testing",0,"https:\u002F\u002Fgithub.com\u002Fkasuboski\u002Fpig","https:\u002F\u002Fgithub.com\u002Fkasuboski","www.joshkasuboski.com","2026-10-13 20:40:10",{"source":8,"skillName":5,"commands":51,"prompt":61,"zipAvailable":62},[52,55,58],{"tool":53,"command":54},"npx","npx skills add kasuboski\u002Fpig --skill gleam-testing",{"tool":56,"command":57},"bunx","bunx skills add kasuboski\u002Fpig --skill gleam-testing",{"tool":59,"command":60},"pnpm","pnpm dlx skills add kasuboski\u002Fpig --skill gleam-testing","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fkasuboski\u002Fpig\u002Fblob\u002Fmain\u002F.pi\u002Fskills\u002Fgleam-testing\u002FSKILL.md",true,[],[65,77,87,95,103,114],{"skillNo":66,"name":67,"nameZh":6,"description":68,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":69,"categories":70,"keywords":74,"publishedAt":37},"01M71Q93PSKKXVQ057YVFGTX7R","fixture-skill","skill description","https:\u002F\u002Fgithub.com\u002Fkasuboski\u002Fpig\u002Fblob\u002Fmain\u002Fpackages\u002Fpig\u002Ftest_data\u002Ftracing\u002Ftool_prompt_skill\u002FSKILL.md",[71],{"categoryNo":72,"categoryName":73,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[75,76],"fixture","fixture skill",{"skillNo":78,"name":79,"nameZh":6,"description":80,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":81,"categories":82,"keywords":84,"publishedAt":37},"01M71Q92S1V8D3NAR8ZH766R9J","valid-skill","A valid skill for testing purposes.","https:\u002F\u002Fgithub.com\u002Fkasuboski\u002Fpig\u002Fblob\u002Fmain\u002Fpackages\u002Fpig\u002Ftest_data\u002Fskills\u002Fvalid-skill\u002FSKILL.md",[83],{"categoryNo":72,"categoryName":73,"taxonomyType":21,"taxonomyTypeDesc":22},[85,86,24,31],"valid","valid skill",{"skillNo":88,"name":89,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":90,"categories":91,"keywords":92,"publishedAt":37},"01M71Q910SJDFHXY5T22K4BXZS","no-frontmatter","https:\u002F\u002Fgithub.com\u002Fkasuboski\u002Fpig\u002Fblob\u002Fmain\u002Fpackages\u002Fpig\u002Ftest_data\u002Fskills\u002Fno-frontmatter\u002FSKILL.md",[],[93,94],"frontmatter","no frontmatter",{"skillNo":96,"name":97,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":98,"categories":99,"keywords":100,"publishedAt":37},"01M71Q8ZSKS0TN3ABKDK3AB99Y","missing-description","https:\u002F\u002Fgithub.com\u002Fkasuboski\u002Fpig\u002Fblob\u002Fmain\u002Fpackages\u002Fpig\u002Ftest_data\u002Fskills\u002Fmissing-description\u002FSKILL.md",[],[101,102],"missing","missing description",{"skillNo":104,"name":105,"nameZh":6,"description":106,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":107,"categories":108,"keywords":109,"publishedAt":37},"01M71Q8XQAFM9MT8Z0A40GAW1C","gleam-expert","Expert Gleam programming patterns and advice.","https:\u002F\u002Fgithub.com\u002Fkasuboski\u002Fpig\u002Fblob\u002Fmain\u002Fpackages\u002Fpig\u002Ftest_data\u002Fskills\u002Fgleam-expert\u002FSKILL.md",[],[110,26,111,30,112,113],"gleam expert","expert","advice","programming",{"skillNo":115,"name":116,"nameZh":6,"description":117,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":118,"categories":119,"keywords":121,"publishedAt":37},"01M71Q8VX98GZEVP3WYB23ZVV9","empty-skill","An empty skill with minimal content.","https:\u002F\u002Fgithub.com\u002Fkasuboski\u002Fpig\u002Fblob\u002Fmain\u002Fpackages\u002Fpig\u002Ftest_data\u002Fskills\u002Fempty-skill\u002FSKILL.md",[120],{"categoryNo":72,"categoryName":73,"taxonomyType":21,"taxonomyTypeDesc":22},[122,123,124,35,125],"empty skill","empty","content","minimal"]