13 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 126 | refactor: extract ExtensionPaths value object from ExtensionRuntime |
Extract ExtensionPaths value object
Problem statement
ExtensionRuntime holds six path fields (agentDir, sessionsDir, subagentSessionsDir, forwardingDir, globalLogsDir, piInfrastructureDirs) that are computed once at startup from agentDir and never mutate.
These fields are threaded individually through HandlerDeps, PermissionPrompter, PermissionForwardingDeps, and isSubagentExecutionContext calls, widening the dependency surface unnecessarily.
Extracting an ExtensionPaths value object is the simplest step in the handler decomposition series (see docs/plans/0126-handler-decomposition.md).
It has zero behavioral risk and sets up later refactorings (#127–#130) to consume a single dep instead of individual fields.
Goals
- Extract an
ExtensionPathsinterface andcomputeExtensionPaths()factory into a newsrc/extension-paths.ts. - Make
ExtensionRuntimeembedExtensionPaths(extends or inline fields) so existing field access continues to work. - Update
createExtensionRuntimeto delegate path computation to the new factory. - Add focused unit tests for
computeExtensionPaths(). - No behavioral change — same permission decisions, same event emissions, same config loading.
Non-goals
- Replacing individual path references in
HandlerDeps,PermissionPrompter, orPermissionForwardingDepswith a singlepaths: ExtensionPathsfield. That is a follow-up refactoring for #129 (PermissionSession) or a later narrowing pass. - Extracting
SessionLogger(#127) orForwardingManager(#128). - Changing the
/permission-systemslash command or any config format.
Background
Permission surface
This change does not touch any permission surface. It is a pure structural extraction of immutable path constants.
Relevant modules
| File | Role in this change |
|---|---|
src/runtime.ts |
Defines ExtensionRuntime interface and createExtensionRuntime(). Path fields are computed inline in the factory. |
src/index.ts |
Composition root — reads runtime.agentDir, runtime.subagentSessionsDir, runtime.forwardingDir to wire PermissionPrompter and PermissionForwardingDeps. |
src/handlers/types.ts |
HandlerDeps carries piInfrastructureDirs as a top-level field. |
src/node-modules-discovery.ts |
Provides discoverGlobalNodeModulesRoot() used to build piInfrastructureDirs. |
tests/runtime.test.ts |
Tests path derivation in createExtensionRuntime — these cover the exact logic being extracted. |
Current path computation (in createExtensionRuntime)
const agentDir = options?.agentDir ?? getAgentDir();
const sessionsDir = join(agentDir, "sessions");
const subagentSessionsDir = join(agentDir, "subagent-sessions");
const forwardingDir = join(sessionsDir, "permission-forwarding");
const globalLogsDir = getGlobalLogsDir(agentDir);
const globalNodeModulesRoot = discoverGlobalNodeModulesRoot();
const piInfrastructureDirs = [
agentDir,
join(agentDir, "git"),
...(globalNodeModulesRoot ? [globalNodeModulesRoot] : []),
];
Design overview
New type and factory
// src/extension-paths.ts
export interface ExtensionPaths {
readonly agentDir: string;
readonly sessionsDir: string;
readonly subagentSessionsDir: string;
readonly forwardingDir: string;
readonly globalLogsDir: string;
readonly piInfrastructureDirs: readonly string[];
}
export function computeExtensionPaths(agentDir: string): ExtensionPaths;
piInfrastructureDirs uses readonly string[] to reflect immutability.
The factory calls getGlobalLogsDir(agentDir) and discoverGlobalNodeModulesRoot() internally — same call sites as today, just relocated.
ExtensionRuntime integration
ExtensionRuntime extends ExtensionPaths (it already declares each field individually as readonly).
After the extraction, the interface declaration drops the six individual readonly field declarations and replaces them with extends ExtensionPaths.
SessionState is unchanged — it does not carry path fields.
No downstream signature changes
Callers that read runtime.agentDir or runtime.piInfrastructureDirs continue to work unchanged because ExtensionRuntime extends ExtensionPaths preserves all the same fields.
HandlerDeps.piInfrastructureDirs and the wiring in index.ts are untouched in this issue.
Module-level changes
Added
src/extension-paths.ts—ExtensionPathsinterface +computeExtensionPaths()factory.tests/extension-paths.test.ts— focused unit tests for the factory.
Changed
src/runtime.ts:- Import
ExtensionPathsandcomputeExtensionPathsfrom./extension-paths. - Change
ExtensionRuntimetoextends ExtensionPathsinstead of declaring the six path fields inline. - In
createExtensionRuntime, replace the inline path computation with acomputeExtensionPaths(agentDir)call and spread the result into the runtime object.
- Import
tests/runtime.test.ts:- Path-derivation tests for
createExtensionRuntimeremain as-is (they verify that the runtime object exposes the correct paths). - Add a mock for
../src/extension-pathsif needed, or leave the real implementation sincecomputeExtensionPathsis a pure function with one side-effecting dep (discoverGlobalNodeModulesRoot) that is already mocked.
- Path-derivation tests for
Unchanged
src/index.ts— continues to readruntime.agentDir,runtime.subagentSessionsDir, etc. No change needed.src/handlers/types.ts—HandlerDeps.piInfrastructureDirsstays as-is.- All handler test files —
makeDeps()factories are unaffected. schemas/,config/,docs/architecture/— no changes needed.
Test impact analysis
- New unit tests enabled:
computeExtensionPaths()can be tested independently ofcreateExtensionRuntime. Tests cover: path derivation fromagentDir,piInfrastructureDirscomposition with/withoutglobalNodeModulesRoot, andreadonlysemantics. - Existing tests that become partially redundant: The path-derivation block in
tests/runtime.test.ts("sets agentDir","derives sessionsDir", etc.) now duplicates coverage with the newextension-paths.test.ts. These tests should stay — they verify thatcreateExtensionRuntimecorrectly delegates tocomputeExtensionPathsand surfaces the fields on the runtime object. They can be simplified in a follow-up if desired (assertruntime.agentDir === "/test/agent"is sufficient; the detailed derivation is covered by the lower-level test). - Existing tests that must stay: All handler tests (
tool-call.test.ts,lifecycle.test.ts, etc.) andruntime.test.tstests for mutable state, logging, config refresh, and agent name resolution are unchanged.
TDD order
Cycle 1: Add ExtensionPaths interface and computeExtensionPaths factory with tests
- Create
tests/extension-paths.test.tswith red tests:computeExtensionPathssetsagentDirfrom argument.- Derives
sessionsDirasjoin(agentDir, "sessions"). - Derives
subagentSessionsDirasjoin(agentDir, "subagent-sessions"). - Derives
forwardingDirasjoin(sessionsDir, "permission-forwarding"). - Derives
globalLogsDirviagetGlobalLogsDir(agentDir). - Includes
agentDirandagentDir/gitinpiInfrastructureDirs. - Includes discovered global
node_modulesroot when present. - Omits global
node_moduleswhen discovery returnsnull. - All entries in
piInfrastructureDirsare strings (nonull).
- Create
src/extension-paths.tswith theExtensionPathsinterface andcomputeExtensionPaths()factory to make tests green. - Commit:
test: add ExtensionPaths unit testsandfeat: extract ExtensionPaths value object (#126)(or squash into onefeat:commit).
Cycle 2: Integrate into ExtensionRuntime
- Update
src/runtime.ts:ExtensionRuntime extends ExtensionPaths.createExtensionRuntimecallscomputeExtensionPaths(agentDir)and spreads into the runtime literal.- Remove the now-redundant inline path computation and the direct import of
discoverGlobalNodeModulesRoot.
- Run existing
tests/runtime.test.ts— all path tests should stay green because the runtime object still exposes the same fields. ThediscoverGlobalNodeModulesRootmock inruntime.test.tsmay need to be replaced with a mock on../src/extension-paths(or left as-is if the realcomputeExtensionPathsis called through and the existing mock of../src/node-modules-discoverystill intercepts correctly). - Run
pnpm run buildto verify type-checking. - Run full test suite.
- Commit:
refactor: use computeExtensionPaths in createExtensionRuntime (#126).
Risks and mitigations
| Risk | Mitigation |
|---|---|
| Could this silently weaken a permission? | No. Pure structural extraction — same path values computed from the same inputs. No gate logic, no policy evaluation, no config loading changes. |
piInfrastructureDirs type narrows from string[] to readonly string[] |
readonly string[] is assignable to string[] consumers. If any caller mutates the array (none do today), the compiler will flag it. This is a safety improvement. |
discoverGlobalNodeModulesRoot mock in runtime.test.ts stops working after extraction |
If createExtensionRuntime no longer calls discoverGlobalNodeModulesRoot directly (it delegates to computeExtensionPaths), the mock target shifts. Either mock ../src/extension-paths in runtime.test.ts, or let the real computeExtensionPaths run and keep the existing mock on ../src/node-modules-discovery which it transitively calls. The latter is simpler and tests the integration. |
| Re-export needed for downstream consumers | ExtensionPaths should be re-exported from src/runtime.ts (or the package barrel if one exists) so index.ts and future consumers can import it without knowing the internal module. |
Open questions
- Should
computeExtensionPathsalso accept an optionalglobalNodeModulesRootparameter (for testability) or always calldiscoverGlobalNodeModulesRoot()internally? Recommendation: accept it as an optional parameter defaulting to the discovery call, matching the pattern used bycreateExtensionRuntime'sagentDiroption. Decide at implementation time based on test ergonomics.