@rubytech/create-maxy-code 0.1.64 → 0.1.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/docs/references/admin-session.md +4 -2
  3. package/payload/platform/plugins/docs/references/internals.md +7 -1
  4. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/_helpers/emit-capture.d.ts +22 -0
  5. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/_helpers/emit-capture.d.ts.map +1 -0
  6. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/_helpers/emit-capture.js +44 -0
  7. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/_helpers/emit-capture.js.map +1 -0
  8. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights-emit.test.d.ts +2 -0
  9. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights-emit.test.d.ts.map +1 -0
  10. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights-emit.test.js +68 -0
  11. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights-emit.test.js.map +1 -0
  12. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection-emit.test.d.ts +2 -0
  13. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection-emit.test.d.ts.map +1 -0
  14. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection-emit.test.js +48 -0
  15. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection-emit.test.js.map +1 -0
  16. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-memory-expunge-emit.test.d.ts +2 -0
  17. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-memory-expunge-emit.test.d.ts.map +1 -0
  18. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-memory-expunge-emit.test.js +53 -0
  19. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-memory-expunge-emit.test.js.map +1 -0
  20. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/graph-prune-denylist-add-emit.test.d.ts +2 -0
  21. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/graph-prune-denylist-add-emit.test.d.ts.map +1 -0
  22. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/graph-prune-denylist-add-emit.test.js +40 -0
  23. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/graph-prune-denylist-add-emit.test.js.map +1 -0
  24. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/graph-prune-denylist-remove-emit.test.d.ts +2 -0
  25. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/graph-prune-denylist-remove-emit.test.d.ts.map +1 -0
  26. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/graph-prune-denylist-remove-emit.test.js +39 -0
  27. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/graph-prune-denylist-remove-emit.test.js.map +1 -0
  28. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write-emit.test.d.ts +2 -0
  29. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write-emit.test.d.ts.map +1 -0
  30. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write-emit.test.js +88 -0
  31. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write-emit.test.js.map +1 -0
  32. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-classify-emit.test.d.ts +2 -0
  33. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-classify-emit.test.d.ts.map +1 -0
  34. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-classify-emit.test.js +58 -0
  35. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-classify-emit.test.js.map +1 -0
  36. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-emit.test.d.ts +2 -0
  37. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-emit.test.d.ts.map +1 -0
  38. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-emit.test.js +58 -0
  39. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-emit.test.js.map +1 -0
  40. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-edit-attachment-emit.test.d.ts +2 -0
  41. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-edit-attachment-emit.test.d.ts.map +1 -0
  42. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-edit-attachment-emit.test.js +70 -0
  43. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-edit-attachment-emit.test.js.map +1 -0
  44. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-emit.test.d.ts +2 -0
  45. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-emit.test.d.ts.map +1 -0
  46. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-emit.test.js +48 -0
  47. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-emit.test.js.map +1 -0
  48. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-emit.test.d.ts +2 -0
  49. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-emit.test.d.ts.map +1 -0
  50. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-emit.test.js +74 -0
  51. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-emit.test.js.map +1 -0
  52. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-extract-emit.test.d.ts +2 -0
  53. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-extract-emit.test.d.ts.map +1 -0
  54. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-extract-emit.test.js +47 -0
  55. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-extract-emit.test.js.map +1 -0
  56. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-web-emit.test.d.ts +2 -0
  57. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-web-emit.test.d.ts.map +1 -0
  58. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-web-emit.test.js +65 -0
  59. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-web-emit.test.js.map +1 -0
  60. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-reindex-emit.test.d.ts +2 -0
  61. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-reindex-emit.test.d.ts.map +1 -0
  62. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-reindex-emit.test.js +43 -0
  63. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-reindex-emit.test.js.map +1 -0
  64. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-rename-attachment-emit.test.d.ts +2 -0
  65. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-rename-attachment-emit.test.d.ts.map +1 -0
  66. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-rename-attachment-emit.test.js +65 -0
  67. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-rename-attachment-emit.test.js.map +1 -0
  68. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-restore-emit.test.d.ts +2 -0
  69. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-restore-emit.test.d.ts.map +1 -0
  70. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-restore-emit.test.js +49 -0
  71. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-restore-emit.test.js.map +1 -0
  72. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.d.ts +2 -0
  73. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.d.ts.map +1 -0
  74. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js +74 -0
  75. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js.map +1 -0
  76. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.d.ts +2 -0
  77. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.d.ts.map +1 -0
  78. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js +74 -0
  79. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js.map +1 -0
  80. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-delete-emit.test.d.ts +2 -0
  81. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-delete-emit.test.d.ts.map +1 -0
  82. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-delete-emit.test.js +62 -0
  83. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-delete-emit.test.js.map +1 -0
  84. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-emit.test.d.ts +2 -0
  85. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-emit.test.d.ts.map +1 -0
  86. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-emit.test.js +76 -0
  87. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-emit.test.js.map +1 -0
  88. package/payload/platform/services/claude-session-manager/dist/index.js +75 -2
  89. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  90. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +8 -0
  91. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  92. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +14 -1
  93. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  94. package/payload/platform/services/claude-session-manager/dist/specialist-drift.d.ts +24 -4
  95. package/payload/platform/services/claude-session-manager/dist/specialist-drift.d.ts.map +1 -1
  96. package/payload/platform/services/claude-session-manager/dist/specialist-drift.js +39 -9
  97. package/payload/platform/services/claude-session-manager/dist/specialist-drift.js.map +1 -1
  98. package/payload/server/server-init.cjs +40 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.64",
3
+ "version": "0.1.65",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -109,7 +109,7 @@ Body overrides relative to a plain Sidebar click:
109
109
 
110
110
  **Hook recursion gate.** `pty-spawner` stamps `MAXY_SPECIALIST=<specialist>` on every PTY env. The Stop hook short-circuits when `MAXY_SPECIALIST=database-operator` so the recorder's own end-of-turn does not re-fire the hook.
111
111
 
112
- **Observability.** The hook emits exactly two lines per operator turn via `/api/admin/log-ingest`: `[turn-recorder] trigger sessionId=<op> turnIndex=0 transcriptBytes=<n> conversationBytes=<n>` and `[turn-recorder] spawn-request sessionId=<op> specialist=database-operator initialMessageBytes=<n>`. The manager adds `pty-spawn-allowlist specialist=database-operator count=11 sourced-from=agent-frontmatter` and `pty-spawn-start … specialist=database-operator append-system-prompt-bytes=0` before the PTY launches. The remainder of the recorder lifecycle is covered by the Sidebar's existing lines (`pty-spawned`, JSONL events, `auto-archive`). Failure-mode names: `trigger-skipped reason=…` enumerates `role-not-admin | is-recorder | empty-stdin | missing-transcript | conversation-empty`. Hook-side spawn errors emit `[turn-recorder] spawn-failed reason=loopback-http http=<code>`.
112
+ **Observability.** The hook emits exactly two lines per operator turn via `/api/admin/log-ingest`: `[turn-recorder] trigger sessionId=<op> turnIndex=0 transcriptBytes=<n> conversationBytes=<n>` and `[turn-recorder] spawn-request sessionId=<op> specialist=database-operator initialMessageBytes=<n>`. The manager adds `pty-spawn-allowlist specialist=database-operator count=11 stripped=0 sourced-from=agent-frontmatter` and `pty-spawn-start … specialist=database-operator append-system-prompt-bytes=0` before the PTY launches. The remainder of the recorder lifecycle is covered by the Sidebar's existing lines (`pty-spawned`, JSONL events, `auto-archive`). Failure-mode names: `trigger-skipped reason=…` enumerates `role-not-admin | is-recorder | empty-stdin | missing-transcript | conversation-empty`. Hook-side spawn errors emit `[turn-recorder] spawn-failed reason=loopback-http http=<code>`.
113
113
 
114
114
  ### Recorder lifecycle diagnostic
115
115
 
@@ -118,7 +118,7 @@ When the operator reports "the recorder did nothing this turn", run these six gr
118
118
  1. **Stop hook fired.** `grep '\[turn-recorder\] trigger sessionId=<op>' ~/.<brand>/logs/server.log` — expects one line of shape `[turn-recorder] trigger sessionId=<op> turnIndex=0 transcriptBytes=<n> conversationBytes=<n>`. Absent: the Stop hook didn't run; check that `MAXY_SESSION_ROLE=admin` was set on the operator PTY and that `MAXY_SPECIALIST!=database-operator` (recursion gate).
119
119
  2. **/spawn accepted.** `grep '\[turn-recorder\] spawn-request sessionId=<op>' ~/.<brand>/logs/server.log` — expects `[turn-recorder] spawn-request sessionId=<op> specialist=database-operator initialMessageBytes=<n>`. The response body of this POST carries `<rec>`. Absent: the hook fired but `/api/admin/claude-sessions` rejected; look for `[turn-recorder] spawn-failed reason=loopback-http http=<code>` on the next line.
120
120
  3. **PTY started.** `grep 'pty-spawn-start .* specialist=database-operator' ~/.<brand>/logs/server.log` — expects `pty-spawn-start claudeBin=<…> argv-count=<n> append-system-prompt-bytes=0 … specialist=database-operator prompt-positional=yes prompt-bytes=<n>`. Absent: the wrapper accepted but the manager rejected before exec; check the spawn-failure surfaces (`which-claude-not-found | pty-spawn-failed | pid-file-timeout | host-context-unresolved | identity-unresolved | mcp-config-write-failed`).
121
- 4. **Agent file resolved.** `grep 'pty-spawn-allowlist specialist=database-operator count=11 sourced-from=agent-frontmatter' ~/.<brand>/logs/server.log` — expects exactly one line per recorder spawn. Absent (or `count` not equal to the frontmatter's tool count): the agent file at `$CLAUDE_CONFIG_DIR/agents/database-operator.md` is missing the `tools:` line, or the symlink the installer creates never landed. Re-run the installer.
121
+ 4. **Agent file resolved.** `grep 'pty-spawn-allowlist specialist=database-operator count=11 stripped=0 sourced-from=agent-frontmatter' ~/.<brand>/logs/server.log` — expects exactly one line per recorder spawn. Absent (or `count` not equal to the frontmatter's tool count, or `stripped>0` meaning the brand excluded a plugin the recorder template references — Task 173): the agent file at `$CLAUDE_CONFIG_DIR/agents/database-operator.md` is missing the `tools:` line, the symlink the installer creates never landed, or the brand-aware drift filter dropped tools. Re-run the installer; cross-reference `brand.json#plugins.excluded` against the agent frontmatter.
122
122
  5. **Graph write outcome.** `grep '\[mcp:memory\] memory-write .* session=<rec>' ~/.<brand>/logs/server.log` — expects one line ending `result=ok elementId=<id>` or `result=error reason=<slug>` (slug enumerated by the memory MCP write-path observability work). Absent: the recorder loaded but produced no tool calls — model-side decision, not infra. Read the recorder JSONL at `<accountDir>/.claude/projects/<slug>/<rec>.jsonl` to see what the LLM did.
123
123
  6. **Auto-archive.** `grep 'auto-archive .* sessionId=<rec> .* specialist=database-operator reason=end-turn' ~/.<brand>/logs/server.log` — expects one line. Absent: the recorder finished but the manager's end-turn watcher didn't fire; the fs-watcher row will get reaped on its TTL but the recorder hung longer than expected. Investigate `pty-spawn-stop` and `pid-file-removed` lines on the same `<rec>`.
124
124
 
@@ -163,6 +163,8 @@ grep '\[mcp:memory\] log-ingest-failed' ~/.${brand}/logs/mcp-memory-stderr-*.log
163
163
 
164
164
  The existing `mcp-memory-stderr-<date>.log` capture is unchanged on error paths — the new structured line is additive, not a relocation. Crash-mode debugging continues to read the stderr log; routine "did it land?" questions read `server.log`.
165
165
 
166
+ **Test contract (Task 168).** Each of the 20 wired tools carries a `*-emit.test.ts` fixture under `platform/plugins/memory/mcp/src/tools/__tests__/` that mocks `fetch` and asserts the wire body. A refactor that drops `withWriteEvent` from any tool fails the matching tool's tests with a clear `expected "vi.fn()" to be called 1 times, but got 0 times` error, naming the tool. A shared helper at `__tests__/_helpers/emit-capture.ts` standardises the fetch capture + Neo4j Session stub so each tool test stays a few lines of mocks + one happy + one error assertion. Drift between the helper's body shape and the route's validator is pinned by an end-to-end test at `platform/ui/server/routes/admin/__tests__/log-ingest-mcp-end-to-end.test.ts`, which routes the helper's POST through the real Hono `log-ingest` route with a loopback `remoteAddress` shim.
167
+
166
168
  ## Out of scope
167
169
 
168
170
  - Public/WhatsApp/Telegram sessions — out of scope. Their sessionKeys remain `crypto.randomUUID()` and follow the existing rejection-on-restart contract.
@@ -492,7 +492,13 @@ For subagents, the same spawn pushes `--add-dir` for every bundled plugin agents
492
492
 
493
493
  A boot gate refuses to start the manager when any admin-allowlisted tool `mcp__<plugin>__*` lacks a registered server. The signal is `boot-failed reason=mcp-allowlist-without-server plugin=<p> tool=<t>` followed by `process.exit(1)`. The remediation is a one-line edit to the named `PLUGIN.md`: add the `mcp:` block. The complementary observability emit `mcp-config-allowlist-coverage admin-tools=A admin-registered=R` (where `A === R`) confirms the invariant per boot.
494
494
 
495
- **Per-spawn signals (server.log).** Every spawn emits `pty-spawn-mcp-config servers=<N> tools=<M> bytes=<B> path=<…>` once, plus one `pty-spawn-agents-dir role=<admin|public> path=<…>` per added directory. The diagnostic one-liner is `grep -E 'pty-spawn-mcp-config|pty-spawn-agents-dir|mcp-config-allowlist-coverage|boot-failed reason=mcp-allowlist' ~/.<brand>/logs/server.log | tail -50`.
495
+ A second boot gate walks every specialist `.md` under `platform/templates/specialists/agents/`, every bundled `<plugin>/agents/` directory, and the per-account `<accountDir>/specialists/agents/` directory, parses each file's `tools:` frontmatter line, and classifies every tool name as one of: CC-native (Read, Bash, …), bridge-namespace (`mcp__plugin_playwright_*`, `mcp__graph__*`), a tool the loaded `PLUGIN.md` set actually serves, `unknown-tool-in-plugin` (namespace served but tool name absent), `unknown-plugin-namespace` (namespace served by nothing), `brand-excluded-plugin` (namespace served by nothing on this brand, **but** the brand's `brand.json#plugins.excluded` list names it), or `malformed-name` (not CC-native and not `mcp__<plugin>__<tool>`-shaped). The first three pass. The next two refuse boot with one `boot-failed reason=specialist-tool-drift specialist=<name> tool=<t> drift=<class> path=<…>` line per defect, then `process.exit(1)`. `brand-excluded-plugin` is a structural pass: it lands in a per-specialist strip-list, the manager continues to boot, and at spawn time `pty-spawner` removes those tool names from the `--agent <name>` spawn's `--allowed-tools` argv. The complementary observability emit `specialist-tool-strip specialist=<name> plugin=<p> tools=<csv> reason=brand-excluded` fires one line per stripped (specialist, plugin) pair so an operator who reads `server.log` sees the brand filter doing work without cross-referencing `brand.json` against the template. The startup-self-test line `startup-self-test specialist-tool-drift=ok inspected=<N> stripped-specialists=<M>` confirms the gate ran and how many specialists carry strip-lists.
496
+
497
+ This gate was Task 173. The `brand-excluded` branch closes the recurring crash-restart loop on brands that ship without a plugin the shared `personal-assistant.md` template references (e.g. `realagent-code` excludes `telegram` while the template hard-codes `mcp__telegram__*`). The brand-agnostic template stays a single file; the brand-aware filter expresses what the specialist *may* do on this install while the template expresses what it *can* do across brands. Tool typos and renamed plugins still refuse to boot — only namespaces explicitly named in `plugins.excluded` are demoted to strip-and-warn.
498
+
499
+ **Per-spawn signals (server.log).** Every spawn emits `pty-spawn-mcp-config servers=<N> tools=<M> bytes=<B> path=<…>` once, plus one `pty-spawn-agents-dir role=<admin|public> path=<…>` per added directory. Specialist spawns additionally emit `pty-spawn-allowlist specialist=<name> count=<N> stripped=<S> sourced-from=agent-frontmatter` where `stripped` is the count of brand-excluded tool names removed before argv emission. The diagnostic one-liner is `grep -E 'pty-spawn-mcp-config|pty-spawn-agents-dir|pty-spawn-allowlist|mcp-config-allowlist-coverage|specialist-tool-strip|boot-failed reason=' ~/.<brand>/logs/server.log | tail -50`.
500
+
501
+ **Brand-process start counter (Task 173).** `platform/ui/server-init.cjs` increments a persistent counter at `/tmp/server-init-<accountId>-restart.count` on every fresh start and emits `[server-init] start count=<N> account=<accountId> counter-path=<…>` to `server.log`. /tmp clears on reboot, so a clean reboot starts the count fresh; any value `>1` between operator-observed reboots means the brand process (driven by its `Requires=<brand>-claude-session-manager.service` clause) is restarting. The diagnostic one-liner is `grep '\[server-init\] start' ~/.<brand>/logs/server.log | tail -5` — the trailing `count=` value is the loop depth without counting SIGTERMs.
496
502
 
497
503
  **Programmatic spawn entry point.** Every admin PTY spawn that needs a first user prompt — UI click, turn-recorder hook, future automation — routes through the single wrapper at [`platform/ui/server/routes/admin/claude-sessions.ts`](../../../ui/server/routes/admin/claude-sessions.ts). The wrapper owns the per-spawn enrichment (owner profile, dormant/active plugins, specialist domains, tunnel URL) and the `senderId` resolution; it forwards a single `POST /spawn` to the session manager on `127.0.0.1`, with `initialMessage` inlined on that body. The manager appends `initialMessage` as the trailing positional argv to `claude`, so the CLI processes it as the session's first user turn at PTY startup — no separate `POST /<sessionId>/input` call, no bracketed-paste. (Task 153.) See `admin-session.md` "Spawn-with-initialMessage wrapper" for the body schema and caller list.
498
504
 
@@ -0,0 +1,22 @@
1
+ import { vi } from "vitest";
2
+ export interface CapturedEmit {
3
+ tag: string;
4
+ level: "info" | "error" | "warn" | "debug";
5
+ line: string;
6
+ }
7
+ export interface EmitCapture {
8
+ fetchSpy: ReturnType<typeof vi.fn>;
9
+ captures: () => CapturedEmit[];
10
+ restore: () => void;
11
+ }
12
+ export declare function setupEmitCapture(port?: string): EmitCapture;
13
+ /**
14
+ * Build a Neo4j Session stub whose `.run()` returns records constructed from
15
+ * row objects. Each call to `.run()` consumes the next entry from rowSequence.
16
+ * A row is a plain object — its keys are accessed by `record.get(key)`.
17
+ */
18
+ export declare function makeMockSession(rowSequence?: Array<Array<Record<string, unknown>>>): {
19
+ run: import("vitest").Mock<import("@vitest/spy").Procedure>;
20
+ close: import("vitest").Mock<import("@vitest/spy").Procedure>;
21
+ };
22
+ //# sourceMappingURL=emit-capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-capture.d.ts","sourceRoot":"","sources":["../../../../src/tools/__tests__/_helpers/emit-capture.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE5B,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACnC,QAAQ,EAAE,MAAM,YAAY,EAAE,CAAC;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,SAAU,GAAG,WAAW,CAmB5D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,WAAW,GAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAM;;;EAatF"}
@@ -0,0 +1,44 @@
1
+ // Task 168 — shared test helper for per-tool emit assertions.
2
+ //
3
+ // Spies global fetch, sets PLATFORM_PORT so withWriteEvent reaches the
4
+ // loopback URL (instead of stderr-fallback), and exposes the captured
5
+ // POST bodies. Each test pairs this with whatever per-tool deps it must
6
+ // mock so the wrapped body either resolves or throws.
7
+ import { vi } from "vitest";
8
+ export function setupEmitCapture(port = "12345") {
9
+ process.env.PLATFORM_PORT = port;
10
+ const originalFetch = globalThis.fetch;
11
+ const fetchSpy = vi.fn().mockResolvedValue(new Response('{"ok":true}', { status: 200 }));
12
+ globalThis.fetch = fetchSpy;
13
+ return {
14
+ fetchSpy,
15
+ captures: () => fetchSpy.mock.calls.map((call) => {
16
+ const init = call[1];
17
+ return JSON.parse(init.body);
18
+ }),
19
+ restore: () => {
20
+ globalThis.fetch = originalFetch;
21
+ delete process.env.PLATFORM_PORT;
22
+ },
23
+ };
24
+ }
25
+ /**
26
+ * Build a Neo4j Session stub whose `.run()` returns records constructed from
27
+ * row objects. Each call to `.run()` consumes the next entry from rowSequence.
28
+ * A row is a plain object — its keys are accessed by `record.get(key)`.
29
+ */
30
+ export function makeMockSession(rowSequence = []) {
31
+ let i = 0;
32
+ return {
33
+ run: vi.fn().mockImplementation(async () => {
34
+ const rows = rowSequence[i++] ?? [];
35
+ return {
36
+ records: rows.map((row) => ({
37
+ get: (key) => row[key],
38
+ })),
39
+ };
40
+ }),
41
+ close: vi.fn().mockResolvedValue(undefined),
42
+ };
43
+ }
44
+ //# sourceMappingURL=emit-capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-capture.js","sourceRoot":"","sources":["../../../../src/tools/__tests__/_helpers/emit-capture.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,uEAAuE;AACvE,sEAAsE;AACtE,wEAAwE;AACxE,sDAAsD;AAEtD,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAc5B,MAAM,UAAU,gBAAgB,CAAC,IAAI,GAAG,OAAO;IAC7C,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,IAAI,CAAC;IACjC,MAAM,aAAa,GAAG,UAAU,CAAC,KAAK,CAAC;IACvC,MAAM,QAAQ,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CACxC,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAC7C,CAAC;IACF,UAAU,CAAC,KAAK,GAAG,QAAmC,CAAC;IACvD,OAAO;QACL,QAAQ;QACR,QAAQ,EAAE,GAAG,EAAE,CACb,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAgB,CAAC;YACpC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAc,CAAiB,CAAC;QACzD,CAAC,CAAC;QACJ,OAAO,EAAE,GAAG,EAAE;YACZ,UAAU,CAAC,KAAK,GAAG,aAAa,CAAC;YACjC,OAAO,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;QACnC,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,cAAqD,EAAE;IACrF,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO;QACL,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,KAAK,IAAI,EAAE;YACzC,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;YACpC,OAAO;gBACL,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;oBAC1B,GAAG,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC;iBAC/B,CAAC,CAAC;aACJ,CAAC;QACJ,CAAC,CAAC;QACF,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC;KAC5C,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=conversation-archive-derive-insights-emit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-archive-derive-insights-emit.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/conversation-archive-derive-insights-emit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,68 @@
1
+ // Task 168 — per-tool emit assertion for conversation-archive-derive-insights.
2
+ //
3
+ // Read-only inside the wrapper but routed through it because Task 166 wired
4
+ // every write-path tool — here it pins observability for the dispatch path,
5
+ // not data writes. Happy path requires real chunks + Haiku; error path is
6
+ // the natural archive-not-found gate.
7
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
8
+ import { setupEmitCapture, makeMockSession } from "./_helpers/emit-capture.js";
9
+ const mockSession = makeMockSession();
10
+ vi.mock("../../lib/neo4j.js", () => ({
11
+ getSession: () => mockSession,
12
+ }));
13
+ vi.mock("../../../../../../lib/oauth-llm/dist/index.js", () => ({
14
+ callOauthLlm: vi.fn(),
15
+ }));
16
+ import { conversationArchiveDeriveInsights } from "../conversation-archive-derive-insights.js";
17
+ describe("conversation-archive-derive-insights — emit wrapper wired", () => {
18
+ let emit;
19
+ beforeEach(() => {
20
+ emit = setupEmitCapture();
21
+ mockSession.run.mockReset();
22
+ });
23
+ afterEach(() => emit.restore());
24
+ it("emits result=error line when archive not found", async () => {
25
+ // loadArchive's MATCH returns no rows → tool throws archive-not-found.
26
+ mockSession.run.mockResolvedValue({ records: [] });
27
+ await expect(conversationArchiveDeriveInsights({
28
+ accountId: "acct-1",
29
+ archiveElementId: "4:missing:1",
30
+ })).rejects.toThrow(/archive-not-found/);
31
+ expect(emit.fetchSpy).toHaveBeenCalledTimes(1);
32
+ const body = emit.captures()[0];
33
+ expect(body.tag).toBe("mcp:memory");
34
+ expect(body.level).toBe("error");
35
+ expect(body.line).toContain("conversation-archive-derive-insights");
36
+ expect(body.line).toContain("result=error");
37
+ });
38
+ it("emits result=ok line on happy path (archive with no chunks to walk)", async () => {
39
+ // loadArchive: one row with archive metadata + totalChunks=1 (totalChunks=0
40
+ // is treated as archive-not-found per loadArchive guard).
41
+ mockSession.run.mockResolvedValueOnce({
42
+ records: [
43
+ {
44
+ get: (k) => {
45
+ if (k === "title")
46
+ return "Stub Archive";
47
+ if (k === "totalChunks")
48
+ return 1;
49
+ return null;
50
+ },
51
+ },
52
+ ],
53
+ });
54
+ // loadChunks: empty — for-loop never iterates so no LLM/related-cypher fires.
55
+ mockSession.run.mockResolvedValueOnce({ records: [] });
56
+ await conversationArchiveDeriveInsights({
57
+ accountId: "acct-1",
58
+ archiveElementId: "4:archive:1",
59
+ });
60
+ expect(emit.fetchSpy).toHaveBeenCalledTimes(1);
61
+ const body = emit.captures()[0];
62
+ expect(body.tag).toBe("mcp:memory");
63
+ expect(body.level).toBe("info");
64
+ expect(body.line).toContain("conversation-archive-derive-insights");
65
+ expect(body.line).toContain("result=ok");
66
+ });
67
+ });
68
+ //# sourceMappingURL=conversation-archive-derive-insights-emit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-archive-derive-insights-emit.test.js","sourceRoot":"","sources":["../../../src/tools/__tests__/conversation-archive-derive-insights-emit.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=conversation-archive-enrich-rejection-emit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-archive-enrich-rejection-emit.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/conversation-archive-enrich-rejection-emit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,48 @@
1
+ // Task 168 — per-tool emit assertion for conversation-archive-enrich-rejection.
2
+ //
3
+ // Existing memory MCP test (conversation-archive-enrich-rejection.test.ts)
4
+ // runs the impl against a scratch directory; this companion file isolates
5
+ // the emit-wrapper contract. We mock the underlying derive-insights module's
6
+ // path constants + key helper so the impl runs without needing PLATFORM_ROOT.
7
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
8
+ import { mkdtempSync, rmSync } from "node:fs";
9
+ import { resolve } from "node:path";
10
+ import { tmpdir } from "node:os";
11
+ import { setupEmitCapture } from "./_helpers/emit-capture.js";
12
+ import { conversationArchiveEnrichRejection } from "../conversation-archive-enrich-rejection.js";
13
+ describe("conversation-archive-enrich-rejection — emit wrapper wired", () => {
14
+ let emit;
15
+ let scratch;
16
+ let origRoot;
17
+ beforeEach(() => {
18
+ emit = setupEmitCapture();
19
+ scratch = mkdtempSync(resolve(tmpdir(), "task-168-"));
20
+ origRoot = process.env.PLATFORM_ROOT;
21
+ process.env.PLATFORM_ROOT = resolve(scratch, "platform");
22
+ });
23
+ afterEach(() => {
24
+ emit.restore();
25
+ if (origRoot === undefined)
26
+ delete process.env.PLATFORM_ROOT;
27
+ else
28
+ process.env.PLATFORM_ROOT = origRoot;
29
+ rmSync(scratch, { recursive: true, force: true });
30
+ });
31
+ it("emits result=ok line on happy path (record mode)", async () => {
32
+ await conversationArchiveEnrichRejection({
33
+ accountId: "acct-1",
34
+ archiveElementId: "4:archive:1",
35
+ chunkElementId: "4:chunk:1",
36
+ kind: "mention",
37
+ contentHash: "abcd",
38
+ mode: "record",
39
+ });
40
+ expect(emit.fetchSpy).toHaveBeenCalledTimes(1);
41
+ const body = emit.captures()[0];
42
+ expect(body.tag).toBe("mcp:memory");
43
+ expect(body.level).toBe("info");
44
+ expect(body.line).toContain("conversation-archive-enrich-rejection");
45
+ expect(body.line).toContain("result=ok");
46
+ });
47
+ });
48
+ //# sourceMappingURL=conversation-archive-enrich-rejection-emit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-archive-enrich-rejection-emit.test.js","sourceRoot":"","sources":["../../../src/tools/__tests__/conversation-archive-enrich-rejection-emit.test.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,EAAE;AACF,2EAA2E;AAC3E,0EAA0E;AAC1E,6EAA6E;AAC7E,8EAA8E;AAE9E,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAM,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAE9D,OAAO,EAAE,kCAAkC,EAAE,MAAM,6CAA6C,CAAC;AAEjG,QAAQ,CAAC,4DAA4D,EAAE,GAAG,EAAE;IAC1E,IAAI,IAAyC,CAAC;IAC9C,IAAI,OAAe,CAAC;IACpB,IAAI,QAA4B,CAAC;IAEjC,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,GAAG,gBAAgB,EAAE,CAAC;QAC1B,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC;QACtD,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;QACrC,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,OAAO,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IACH,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;;YACxD,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,QAAQ,CAAC;QAC1C,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,kCAAkC,CAAC;YACvC,SAAS,EAAE,QAAQ;YACnB,gBAAgB,EAAE,aAAa;YAC/B,cAAc,EAAE,WAAW;YAC3B,IAAI,EAAE,SAAS;YACf,WAAW,EAAE,MAAM;YACnB,IAAI,EAAE,QAAQ;SACf,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAE,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACpC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,uCAAuC,CAAC,CAAC;QACrE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=conversation-memory-expunge-emit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-memory-expunge-emit.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/conversation-memory-expunge-emit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,53 @@
1
+ // Task 168 — per-tool emit assertion for conversation-memory-expunge.
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
3
+ import { setupEmitCapture } from "./_helpers/emit-capture.js";
4
+ vi.mock("../../lib/neo4j.js", () => ({
5
+ getDriver: () => ({}),
6
+ }));
7
+ vi.mock("../../lib/graph-prune.js", () => ({
8
+ expungeConversationMemory: vi.fn(),
9
+ }));
10
+ import { conversationMemoryExpunge } from "../conversation-memory-expunge.js";
11
+ import { expungeConversationMemory } from "../../lib/graph-prune.js";
12
+ describe("conversation-memory-expunge — emit wrapper wired", () => {
13
+ let emit;
14
+ beforeEach(() => {
15
+ emit = setupEmitCapture();
16
+ vi.mocked(expungeConversationMemory).mockReset();
17
+ });
18
+ afterEach(() => emit.restore());
19
+ it("emits result=ok line on happy path", async () => {
20
+ vi.mocked(expungeConversationMemory).mockResolvedValue({
21
+ conversationId: "conv-1",
22
+ pattern: ".*",
23
+ nodesScanned: 0,
24
+ propertiesNulled: 0,
25
+ affectedNodeIds: [],
26
+ });
27
+ await conversationMemoryExpunge({
28
+ accountId: "acct-1",
29
+ conversationId: "conv-1",
30
+ pattern: ".*",
31
+ });
32
+ expect(emit.fetchSpy).toHaveBeenCalledTimes(1);
33
+ const body = emit.captures()[0];
34
+ expect(body.tag).toBe("mcp:memory");
35
+ expect(body.level).toBe("info");
36
+ expect(body.line).toContain("conversation-memory-expunge");
37
+ expect(body.line).toContain("result=ok");
38
+ });
39
+ it("emits result=error line when expungeConversationMemory throws", async () => {
40
+ vi.mocked(expungeConversationMemory).mockRejectedValue(new Error("driver error"));
41
+ await expect(conversationMemoryExpunge({
42
+ accountId: "acct-1",
43
+ conversationId: "conv-1",
44
+ pattern: ".*",
45
+ })).rejects.toThrow(/driver error/);
46
+ expect(emit.fetchSpy).toHaveBeenCalledTimes(1);
47
+ const body = emit.captures()[0];
48
+ expect(body.level).toBe("error");
49
+ expect(body.line).toContain("conversation-memory-expunge");
50
+ expect(body.line).toContain("result=error");
51
+ });
52
+ });
53
+ //# sourceMappingURL=conversation-memory-expunge-emit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-memory-expunge-emit.test.js","sourceRoot":"","sources":["../../../src/tools/__tests__/conversation-memory-expunge-emit.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=graph-prune-denylist-add-emit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-prune-denylist-add-emit.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/graph-prune-denylist-add-emit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ // Task 168 — per-tool emit assertion for graph-prune-denylist-add.
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
3
+ import { setupEmitCapture } from "./_helpers/emit-capture.js";
4
+ vi.mock("../../lib/graph-prune.js", () => ({
5
+ resolveConfigDir: () => "/tmp/task-168-stub",
6
+ loadDenylist: () => ({ values: [] }),
7
+ saveDenylist: vi.fn(),
8
+ }));
9
+ import { graphPruneDenylistAdd } from "../graph-prune-denylist-add.js";
10
+ import * as graphPrune from "../../lib/graph-prune.js";
11
+ describe("graph-prune-denylist-add — emit wrapper wired", () => {
12
+ let emit;
13
+ beforeEach(() => {
14
+ emit = setupEmitCapture();
15
+ vi.mocked(graphPrune.saveDenylist).mockReset();
16
+ });
17
+ afterEach(() => emit.restore());
18
+ it("emits result=ok line on happy path", async () => {
19
+ await graphPruneDenylistAdd(["NewLabel"]);
20
+ expect(emit.fetchSpy).toHaveBeenCalledTimes(1);
21
+ const body = emit.captures()[0];
22
+ expect(body.tag).toBe("mcp:memory");
23
+ expect(body.level).toBe("info");
24
+ expect(body.line).toContain("graph-prune-denylist-add");
25
+ expect(body.line).toContain("result=ok");
26
+ });
27
+ it("emits result=error line when saveDenylist throws", async () => {
28
+ vi.mocked(graphPrune.saveDenylist).mockImplementation(() => {
29
+ throw new Error("disk full");
30
+ });
31
+ await expect(graphPruneDenylistAdd(["NewLabel"])).rejects.toThrow(/disk full/);
32
+ expect(emit.fetchSpy).toHaveBeenCalledTimes(1);
33
+ const body = emit.captures()[0];
34
+ expect(body.level).toBe("error");
35
+ expect(body.line).toContain("graph-prune-denylist-add");
36
+ expect(body.line).toContain("result=error");
37
+ expect(body.line).toContain("reason=neo4j-driver-error");
38
+ });
39
+ });
40
+ //# sourceMappingURL=graph-prune-denylist-add-emit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-prune-denylist-add-emit.test.js","sourceRoot":"","sources":["../../../src/tools/__tests__/graph-prune-denylist-add-emit.test.ts"],"names":[],"mappings":"AAAA,mEAAmE;AAEnE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAE9D,EAAE,CAAC,IAAI,CAAC,0BAA0B,EAAE,GAAG,EAAE,CAAC,CAAC;IACzC,gBAAgB,EAAE,GAAG,EAAE,CAAC,oBAAoB;IAC5C,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACpC,YAAY,EAAE,EAAE,CAAC,EAAE,EAAE;CACtB,CAAC,CAAC,CAAC;AAEJ,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,KAAK,UAAU,MAAM,0BAA0B,CAAC;AAEvD,QAAQ,CAAC,+CAA+C,EAAE,GAAG,EAAE;IAC7D,IAAI,IAAyC,CAAC;IAC9C,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,GAAG,gBAAgB,EAAE,CAAC;QAC1B,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,SAAS,EAAE,CAAC;IACjD,CAAC,CAAC,CAAC;IACH,SAAS,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IAEhC,EAAE,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;QAClD,MAAM,qBAAqB,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAE,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACpC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,0BAA0B,CAAC,CAAC;QACxD,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,kBAAkB,CAAC,GAAG,EAAE;YACzD,MAAM,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC;QAC/B,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QAC/E,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAE,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,0BAA0B,CAAC,CAAC;QACxD,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QAC5C,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=graph-prune-denylist-remove-emit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-prune-denylist-remove-emit.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/graph-prune-denylist-remove-emit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ // Task 168 — per-tool emit assertion for graph-prune-denylist-remove.
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
3
+ import { setupEmitCapture } from "./_helpers/emit-capture.js";
4
+ vi.mock("../../lib/graph-prune.js", () => ({
5
+ resolveConfigDir: () => "/tmp/task-168-stub",
6
+ loadDenylist: () => ({ values: ["ExistingLabel"] }),
7
+ saveDenylist: vi.fn(),
8
+ }));
9
+ import { graphPruneDenylistRemove } from "../graph-prune-denylist-remove.js";
10
+ import * as graphPrune from "../../lib/graph-prune.js";
11
+ describe("graph-prune-denylist-remove — emit wrapper wired", () => {
12
+ let emit;
13
+ beforeEach(() => {
14
+ emit = setupEmitCapture();
15
+ vi.mocked(graphPrune.saveDenylist).mockReset();
16
+ });
17
+ afterEach(() => emit.restore());
18
+ it("emits result=ok line on happy path", async () => {
19
+ await graphPruneDenylistRemove(["ExistingLabel"]);
20
+ expect(emit.fetchSpy).toHaveBeenCalledTimes(1);
21
+ const body = emit.captures()[0];
22
+ expect(body.tag).toBe("mcp:memory");
23
+ expect(body.level).toBe("info");
24
+ expect(body.line).toContain("graph-prune-denylist-remove");
25
+ expect(body.line).toContain("result=ok");
26
+ });
27
+ it("emits result=error line when saveDenylist throws", async () => {
28
+ vi.mocked(graphPrune.saveDenylist).mockImplementation(() => {
29
+ throw new Error("permission denied");
30
+ });
31
+ await expect(graphPruneDenylistRemove(["ExistingLabel"])).rejects.toThrow(/permission denied/);
32
+ expect(emit.fetchSpy).toHaveBeenCalledTimes(1);
33
+ const body = emit.captures()[0];
34
+ expect(body.level).toBe("error");
35
+ expect(body.line).toContain("graph-prune-denylist-remove");
36
+ expect(body.line).toContain("result=error");
37
+ });
38
+ });
39
+ //# sourceMappingURL=graph-prune-denylist-remove-emit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-prune-denylist-remove-emit.test.js","sourceRoot":"","sources":["../../../src/tools/__tests__/graph-prune-denylist-remove-emit.test.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAE9D,EAAE,CAAC,IAAI,CAAC,0BAA0B,EAAE,GAAG,EAAE,CAAC,CAAC;IACzC,gBAAgB,EAAE,GAAG,EAAE,CAAC,oBAAoB;IAC5C,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,eAAe,CAAC,EAAE,CAAC;IACnD,YAAY,EAAE,EAAE,CAAC,EAAE,EAAE;CACtB,CAAC,CAAC,CAAC;AAEJ,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,KAAK,UAAU,MAAM,0BAA0B,CAAC;AAEvD,QAAQ,CAAC,kDAAkD,EAAE,GAAG,EAAE;IAChE,IAAI,IAAyC,CAAC;IAC9C,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,GAAG,gBAAgB,EAAE,CAAC;QAC1B,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,SAAS,EAAE,CAAC;IACjD,CAAC,CAAC,CAAC;IACH,SAAS,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IAEhC,EAAE,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;QAClD,MAAM,wBAAwB,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC;QAClD,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAE,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACpC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,6BAA6B,CAAC,CAAC;QAC3D,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,kBAAkB,CAAC,GAAG,EAAE;YACzD,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,wBAAwB,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;QAC/F,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAE,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,6BAA6B,CAAC,CAAC;QAC3D,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=memory-archive-write-emit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-archive-write-emit.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/memory-archive-write-emit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,88 @@
1
+ // Task 168 — per-tool emit assertion for memory-archive-write.
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
3
+ import { setupEmitCapture } from "./_helpers/emit-capture.js";
4
+ const mockSession = {
5
+ run: vi.fn(),
6
+ executeWrite: vi.fn(),
7
+ close: vi.fn().mockResolvedValue(undefined),
8
+ };
9
+ vi.mock("../../lib/neo4j.js", () => ({
10
+ getSession: () => mockSession,
11
+ }));
12
+ import { memoryArchiveWrite } from "../memory-archive-write.js";
13
+ describe("memory-archive-write — emit wrapper wired", () => {
14
+ let emit;
15
+ beforeEach(() => {
16
+ emit = setupEmitCapture();
17
+ mockSession.run.mockReset();
18
+ mockSession.executeWrite.mockReset();
19
+ });
20
+ afterEach(() => emit.restore());
21
+ it("emits result=ok line on happy path", async () => {
22
+ // verifyArchive's MATCH: owner found with :Person label and matching account.
23
+ mockSession.run.mockResolvedValue({
24
+ records: [
25
+ {
26
+ get: (k) => {
27
+ if (k === "labels")
28
+ return ["Person"];
29
+ if (k === "accountId")
30
+ return "acct-1";
31
+ return null;
32
+ },
33
+ },
34
+ ],
35
+ });
36
+ const noopCounters = {
37
+ nodesCreated: 0,
38
+ relationshipsCreated: 0,
39
+ propertiesSet: 0,
40
+ };
41
+ // writeBatch wraps personRes / orgRes inside executeWrite. Return the
42
+ // counter shapes the handler reads.
43
+ mockSession.executeWrite.mockImplementation(async (fn) => {
44
+ return fn({
45
+ run: vi.fn().mockResolvedValue({
46
+ summary: { counters: { updates: () => noopCounters } },
47
+ }),
48
+ });
49
+ });
50
+ await memoryArchiveWrite({
51
+ archiveType: "linkedin-connections",
52
+ ownerNodeId: "4:owner:1",
53
+ accountId: "acct-1",
54
+ rows: [
55
+ {
56
+ firstName: "Alice",
57
+ lastName: "Carter",
58
+ email: "alice@example.com",
59
+ company: "ExampleCo",
60
+ position: "Director",
61
+ connectedOn: "2024-01-01",
62
+ url: "https://www.linkedin.com/in/alice",
63
+ },
64
+ ],
65
+ });
66
+ expect(emit.fetchSpy).toHaveBeenCalledTimes(1);
67
+ const body = emit.captures()[0];
68
+ expect(body.tag).toBe("mcp:memory");
69
+ expect(body.level).toBe("info");
70
+ expect(body.line).toContain("memory-archive-write");
71
+ expect(body.line).toContain("result=ok");
72
+ });
73
+ it("emits result=error line on missing accountId (invalid-input)", async () => {
74
+ await expect(memoryArchiveWrite({
75
+ archiveType: "linkedin-connections",
76
+ ownerNodeId: "4:owner:1",
77
+ accountId: "",
78
+ rows: [{}],
79
+ })).rejects.toThrow(/accountId is required/);
80
+ expect(emit.fetchSpy).toHaveBeenCalledTimes(1);
81
+ const body = emit.captures()[0];
82
+ expect(body.level).toBe("error");
83
+ expect(body.line).toContain("memory-archive-write");
84
+ expect(body.line).toContain("result=error");
85
+ expect(body.line).toContain("reason=invalid-input");
86
+ });
87
+ });
88
+ //# sourceMappingURL=memory-archive-write-emit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-archive-write-emit.test.js","sourceRoot":"","sources":["../../../src/tools/__tests__/memory-archive-write-emit.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=memory-classify-emit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-classify-emit.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/memory-classify-emit.test.ts"],"names":[],"mappings":""}