13 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 127 | refactor: extract SessionLogger interface to unify logging + notification |
Extract SessionLogger interface
Problem statement
Handlers receive three separate logging/notification functions via HandlerDeps: writeDebugLog, writeReviewLog, and notifyWarning.
These are always used together and always wired identically in the composition root (src/index.ts).
They add 3 fields to every makeDeps() test factory across 6 handler test files.
Goals
- Define a
SessionLoggerinterface in a newsrc/session-logger.tsmodule. - Create a
createSessionLogger()factory that wrapscreatePermissionSystemLogger+ the notification callback. - Replace the 3 separate functions in
HandlerDepswith a singlelogger: SessionLoggerfield. - Update all handler files to use
deps.logger.debug(...),deps.logger.review(...),deps.logger.warn(...). - Update all 6 handler test
makeDeps()factories. - No behavioral change — pure extraction.
Non-goals
- Changing
GateRunnerDeps.writeReviewLog— that interface is satisfied by the tool-call handler locally and will be updated whenPermissionSessionsatisfies it directly (#129). - Changing
PermissionPrompterDeps.writeReviewLogorPermissionForwardingDeps.writeReviewLog— those are separate dep interfaces with their own consumers. - Changing
PermissionGateParams.writeLog— that is a generic log callback, not a handler dep. - Extracting
ForwardingManager(#128) orPermissionSession(#129).
Background
This is step 2 of the handler decomposition series (see docs/plans/0126-handler-decomposition.md).
Step 1 (ExtensionPaths, #126) is already implemented and merged.
Steps #128 and #129 are still open; #127 is independent of #128 and a prerequisite for #129.
Permission surface
No permission surface is added, removed, or changed. This is a pure internal refactoring of the handler dependency shape.
Affected files
The three logging fields in HandlerDeps are consumed by:
| Field | Handlers |
|---|---|
writeDebugLog |
lifecycle.ts (2 sites) |
writeReviewLog |
tool-call.ts (4 sites), input.ts (1 site), gates/runner.ts (2 sites) |
notifyWarning |
lifecycle.ts (1 site) |
The composition root (src/index.ts) wires all three from runtime.writeDebugLog/writeReviewLog and runtime.runtimeContext?.ui.notify.
Design overview
SessionLogger interface
/** Unified logging + notification surface for handler deps. */
export interface SessionLogger {
debug(event: string, details?: Record<string, unknown>): void;
review(event: string, details?: Record<string, unknown>): void;
warn(message: string): void;
}
createSessionLogger factory
export function createSessionLogger(
runtime: ExtensionRuntime,
): SessionLogger {
return {
debug: (event, details) => runtime.writeDebugLog(event, details),
review: (event, details) => runtime.writeReviewLog(event, details),
warn: (message) => runtime.runtimeContext?.ui.notify(message, "warning"),
};
}
The factory captures runtime by reference so warn always reads the current runtimeContext (same behavior as the existing notifyWarning closure in index.ts).
HandlerDeps change
export interface HandlerDeps {
// Remove:
// writeDebugLog(event: string, details?: Record<string, unknown>): void;
// writeReviewLog(event: string, details?: Record<string, unknown>): void;
// notifyWarning(message: string): void;
// Add:
readonly logger: SessionLogger;
// ... rest unchanged
}
Handler migration (mechanical)
| Before | After |
|---|---|
deps.writeDebugLog(event, details) |
deps.logger.debug(event, details) |
deps.writeReviewLog(event, details) |
deps.logger.review(event, details) |
deps.notifyWarning(message) |
deps.logger.warn(message) |
const { writeReviewLog } = deps; |
const { review: writeReviewLog } = deps.logger; |
The tool-call handler destructures writeReviewLog from deps and passes it into GateRunnerDeps.
After this change the destructuring reads from deps.logger instead — GateRunnerDeps is unaware of the change.
Test factory migration (mechanical)
Before (3 fields):
writeDebugLog: vi.fn(),
writeReviewLog: vi.fn(),
// ...
notifyWarning: vi.fn(),
After (1 field):
logger: {
debug: vi.fn(),
review: vi.fn(),
warn: vi.fn(),
},
Test assertions change from deps.writeDebugLog to deps.logger.debug, etc.
Module-level changes
New files
| File | Purpose |
|---|---|
src/session-logger.ts |
SessionLogger interface + createSessionLogger() factory |
tests/session-logger.test.ts |
Unit tests for createSessionLogger() |
Changed files — source
| File | Change |
|---|---|
src/handlers/types.ts |
Replace 3 fields with readonly logger: SessionLogger; add import |
src/handlers/lifecycle.ts |
deps.writeDebugLog → deps.logger.debug; deps.notifyWarning → deps.logger.warn |
src/handlers/tool-call.ts |
const { writeReviewLog } = deps → const { review: writeReviewLog } = deps.logger |
src/handlers/input.ts |
deps.writeReviewLog → deps.logger.review |
src/handlers/gates/runner.ts |
deps.writeReviewLog → deps.logger.review (2 sites: session-approved log + writeLog param) |
src/index.ts |
Replace 3 inline closures with logger: createSessionLogger(runtime); add import |
Changed files — tests
| File | Change |
|---|---|
tests/handlers/lifecycle.test.ts |
makeDeps factory + assertions |
tests/handlers/tool-call.test.ts |
makeDeps factory + assertions |
tests/handlers/tool-call-events.test.ts |
makeDeps factory |
tests/handlers/input.test.ts |
makeDeps factory + assertions |
tests/handlers/input-events.test.ts |
makeDeps factory |
tests/handlers/before-agent-start.test.ts |
makeDeps factory (no logging assertions to update) |
Changed files — docs
| File | Change |
|---|---|
docs/architecture/architecture.md |
Update types.ts line in module tree to mention SessionLogger |
Unchanged
src/handlers/gates/descriptor.ts—GateRunnerDeps.writeReviewLogstays as-is.tests/handlers/gates/runner.test.ts— usesGateRunnerDeps, notHandlerDeps.src/permission-prompter.ts— has its ownPermissionPrompterDeps.writeReviewLog.src/forwarded-permissions/— has its ownForwardedPermissionLoggerandPermissionForwardingDeps.src/permission-event-rpc.ts— has its own dep interface.tests/permission-system.test.ts— integration test; never constructsHandlerDeps.
Test impact analysis
- New unit tests enabled:
createSessionLogger()can be tested in isolation — verifydebug/reviewdelegate toruntime.writeDebugLog/writeReviewLog, andwarndelegates toruntime.runtimeContext?.ui.notify(including the null-context case). These were previously untestable because the closures were inline inindex.ts. - Existing tests that become simpler: All 6 handler
makeDeps()factories shrink by 2 net fields (3 removed, 1 added). Assertions on logging behavior get a single parent object (deps.logger) instead of reaching intodepsdirectly. - Existing tests that must stay as-is: All handler behavioral tests stay — they test permission logic, not logging wiring.
GateRunnerDepstests are completely unaffected.
TDD order
Step 1 — SessionLogger interface + createSessionLogger factory
- Red: Write
tests/session-logger.test.ts— test thatcreateSessionLogger()delegatesdebug→runtime.writeDebugLog,review→runtime.writeReviewLog, andwarn→runtime.runtimeContext.ui.notify. Test the null-contextwarnno-op path. - Green: Create
src/session-logger.tswith theSessionLoggerinterface andcreateSessionLogger()factory. - Commit:
feat: add SessionLogger interface and createSessionLogger factory (#127)
Step 2 — Update HandlerDeps and handler source files
- Red:
pnpm run buildfails after updatingHandlerDeps(callers still use old field names). - Green: Update
src/handlers/types.tsto replace the 3 fields withreadonly logger: SessionLogger. Update all handler source files (lifecycle.ts,tool-call.ts,input.ts) andgates/runner.tsto usedeps.logger.*. Updatesrc/index.tsto wirelogger: createSessionLogger(runtime)instead of 3 separate closures. - Verify:
pnpm run buildpasses. Tests still fail (test factories reference old fields). - Commit:
refactor: replace HandlerDeps logging fields with SessionLogger (#127)
Step 3 — Update handler test factories and assertions
- Red:
pnpm vitest runshows failures in all 6 handler test files (old field names inmakeDeps+ assertions). - Green: Update
makeDeps()in each test file to uselogger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() }. Update assertions that referencedeps.writeDebugLog→deps.logger.debug,deps.writeReviewLog→deps.logger.review,deps.notifyWarning→deps.logger.warn. - Verify:
pnpm vitest runpasses.pnpm run buildpasses. - Commit:
test: update handler test factories for SessionLogger (#127)
Step 4 — Update architecture doc
- Green: Update
docs/architecture/architecture.mdmodule tree entry fortypes.ts. - Commit:
docs: update architecture doc for SessionLogger (#127)
Risks and mitigations
| Risk | Mitigation |
|---|---|
| Could silently weaken a permission? | No. Same checkPermission calls, same parameters, same gate evaluation order. Only logging/notification wiring changes. Integration test (permission-system.test.ts) is unaffected. |
| Large blast radius across test files | All 6 handler test file changes are mechanical find-and-replace. Each makeDeps() factory is self-contained. Steps 2 and 3 are separated so source changes compile before test changes land. |
GateRunnerDeps.writeReviewLog type mismatch after rename |
GateRunnerDeps is unchanged. The tool-call handler destructures const { review: writeReviewLog } = deps.logger and passes the function to GateRunnerDeps — no type-level change at the boundary. |
createSessionLogger captures runtime by reference — stale state? |
Same pattern as the existing inline closures in index.ts. warn reads runtime.runtimeContext at call time (not capture time), matching current behavior. |
Open questions
None — the issue description is fully specified and the change is mechanical.