mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
refactor: delete dead infra and config exports (#106019)
* refactor: delete dead infra and config exports * refactor: preserve live infra and config contracts * refactor(config): remove obsolete file-store lifecycle APIs * refactor(infra): finish current-main dead export cleanup
This commit is contained in:
+1
-1
@@ -1961,7 +1961,7 @@
|
||||
"test:unit:fast:audit": "node scripts/test-unit-fast-audit.mjs",
|
||||
"test:voicecall:closedloop": "node scripts/test-voicecall-closedloop.mjs",
|
||||
"test:watch": "node scripts/test-projects.mjs --watch",
|
||||
"test:windows:ci": "node scripts/test-projects.mjs src/shared/runtime-import.test.ts src/process/exec.windows.test.ts src/process/windows-command.test.ts src/infra/backup-create.windows.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/daemon/schtasks.startup-fallback.test.ts extensions/lobster/src/lobster-runner.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
|
||||
"test:windows:ci": "node scripts/test-projects.mjs src/shared/runtime-import.test.ts src/process/exec.windows.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/daemon/schtasks.startup-fallback.test.ts extensions/lobster/src/lobster-runner.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
|
||||
"tool-display:check": "node --import tsx scripts/tool-display.ts --check",
|
||||
"tool-display:write": "node --import tsx scripts/tool-display.ts --write",
|
||||
"ts-topology": "node --import tsx scripts/ts-topology.ts",
|
||||
|
||||
@@ -1311,214 +1311,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
|
||||
"src/commitments/store.ts: saveCommitmentStore",
|
||||
"src/compat/legacy-names.ts: MACOS_APP_SOURCES_DIR",
|
||||
"src/compat/legacy-names.ts: PROJECT_NAME",
|
||||
"src/config/backup-rotation.ts: cleanOrphanBackups",
|
||||
"src/config/backup-rotation.ts: hardenBackupPermissions",
|
||||
"src/config/backup-rotation.ts: rotateConfigBackups",
|
||||
"src/config/channel-config-metadata.ts: ChannelDmPolicyMetadata",
|
||||
"src/config/channel-config-metadata.ts: ChannelSchemaMetadataWithOwnership",
|
||||
"src/config/commands.ts: isCommandFlagEnabled",
|
||||
"src/config/commands.ts: isRestartEnabled",
|
||||
"src/config/cron-limits.ts: DEFAULT_CRON_TRIGGER_MIN_INTERVAL_MS",
|
||||
"src/config/env-vars.ts: collectDurableServiceEnvVars",
|
||||
"src/config/env-vars.ts: readStateDirDotEnvVars",
|
||||
"src/config/includes.ts: deepMerge",
|
||||
"src/config/includes.ts: MAX_INCLUDE_FILE_BYTES",
|
||||
"src/config/includes.ts: MAX_INCLUDE_PATH_LENGTH",
|
||||
"src/config/io.audit.ts: redactConfigAuditArgv",
|
||||
"src/config/io.health-state.ts: ConfigHealthStateDeps",
|
||||
"src/config/io.invalid-config.ts: formatInvalidConfigLogMessage",
|
||||
"src/config/io.invalid-config.ts: logInvalidConfigOnce",
|
||||
"src/config/io.observe-recovery.ts: ObserveRecoveryDeps",
|
||||
"src/config/io.observe-recovery.ts: resolveLastKnownGoodConfigPath",
|
||||
"src/config/io.observe-suspicious.ts: ConfigObserveSuspiciousBaseline",
|
||||
"src/config/io.owner-display-secret.ts: OwnerDisplaySecretRuntimeState",
|
||||
"src/config/io.write-prepare.ts: unsetPathForWrite",
|
||||
"src/config/issue-format.ts: normalizeConfigIssue",
|
||||
"src/config/issue-format.ts: normalizeConfigIssuePath",
|
||||
"src/config/markdown-tables.ts: ResolveMarkdownTableMode",
|
||||
"src/config/markdown-tables.ts: ResolveMarkdownTableModeParams",
|
||||
"src/config/markdown-tables.types.ts: ResolveMarkdownTableMode",
|
||||
"src/config/media-audio-field-metadata.ts: MEDIA_AUDIO_FIELD_KEYS",
|
||||
"src/config/mutate.ts: ConfigMutationBase",
|
||||
"src/config/patch-replace-paths.ts: normalizeConfigPatchReplacePath",
|
||||
"src/config/plugin-auto-enable.shared.ts: PluginAutoEnableCandidate",
|
||||
"src/config/plugin-auto-enable.shared.ts: PluginAutoEnableResult",
|
||||
"src/config/runtime-group-policy.ts: resetMissingProviderGroupPolicyFallbackWarningsForTesting",
|
||||
"src/config/runtime-group-policy.ts: resolveRuntimeGroupPolicy",
|
||||
"src/config/schema-base.ts: BaseConfigSchemaResponse",
|
||||
"src/config/schema.hints.ts: __test__",
|
||||
"src/config/schema.hints.ts: isPluginOwnedChannelHintPath",
|
||||
"src/config/schema.hints.ts: isSensitiveConfigPath",
|
||||
"src/config/schema.tags.ts: CONFIG_TAGS",
|
||||
"src/config/schema.tags.ts: ConfigTag",
|
||||
"src/config/schema.tags.ts: deriveTagsForPath",
|
||||
"src/config/schema.ts: ConfigSchema",
|
||||
"src/config/schema.ts: ConfigSchemaLookupChild",
|
||||
"src/config/schema.ts: ConfigSchemaLookupResult",
|
||||
"src/config/schema.ts: ConfigSchemaReloadKind",
|
||||
"src/config/schema.ts: ConfigSchemaReloadMetadata",
|
||||
"src/config/schema.ts: ConfigSchemaReloadMetadataResolver",
|
||||
"src/config/schema.ts: ConfigUiHint",
|
||||
"src/config/schema.ts: ConfigUiHints",
|
||||
"src/config/sessions.ts: CanonicalizeSessionEntryAliasesResult",
|
||||
"src/config/sessions.ts: DeleteSessionEntryLifecycleParams",
|
||||
"src/config/sessions.ts: DeleteSessionEntryLifecycleResult",
|
||||
"src/config/sessions.ts: ResetSessionEntryLifecycleParams",
|
||||
"src/config/sessions.ts: ResetSessionEntryLifecycleResult",
|
||||
"src/config/sessions.ts: ResolvedSessionEntryCandidateTarget",
|
||||
"src/config/sessions.ts: SessionEntryCandidateAccessScope",
|
||||
"src/config/sessions.ts: SessionLifecycleArchivedTranscript",
|
||||
"src/config/sessions.ts: SessionLifecycleStoreTarget",
|
||||
"src/config/sessions/artifacts.ts: isTrajectoryPointerArtifactName",
|
||||
"src/config/sessions/artifacts.ts: isTrajectoryRuntimeArtifactName",
|
||||
"src/config/sessions/artifacts.ts: parseCompactionCheckpointTranscriptFileName",
|
||||
"src/config/sessions/cleanup-service.ts: SessionCleanupAction",
|
||||
"src/config/sessions/cleanup-service.ts: SessionsCleanupRunResult",
|
||||
"src/config/sessions/disk-budget.ts: SessionDiskBudgetConfig",
|
||||
"src/config/sessions/disk-budget.ts: SessionDiskBudgetLogger",
|
||||
"src/config/sessions/entry-freshness.ts: ResolvedSessionEntryResetFreshness",
|
||||
"src/config/sessions/entry-freshness.ts: ResolveSessionEntryResetFreshnessParams",
|
||||
"src/config/sessions/entry-freshness.ts: SessionEntryLifecycleTimestamps",
|
||||
"src/config/sessions/goals.ts: SessionGoalSnapshot",
|
||||
"src/config/sessions/lifecycle.ts: SESSION_WORK_START_INVALIDATED_ERROR_CODE",
|
||||
"src/config/sessions/paths.ts: SAFE_SESSION_ID_RE",
|
||||
"src/config/sessions/plugin-host-cleanup.ts: PluginHostSessionCleanupMode",
|
||||
"src/config/sessions/reset-policy.ts: DEFAULT_RESET_AT_HOUR",
|
||||
"src/config/sessions/reset-policy.ts: DEFAULT_RESET_MODE",
|
||||
"src/config/sessions/reset-policy.ts: resolveDailyResetAtMs",
|
||||
"src/config/sessions/reset.ts: DEFAULT_RESET_AT_HOUR",
|
||||
"src/config/sessions/reset.ts: DEFAULT_RESET_MODE",
|
||||
"src/config/sessions/reset.ts: isThreadSessionKey",
|
||||
"src/config/sessions/reset.ts: resolveDailyResetAtMs",
|
||||
"src/config/sessions/runtime-types.ts: ReadSessionUpdatedAt",
|
||||
"src/config/sessions/runtime-types.ts: RecordSessionMetaFromInbound",
|
||||
"src/config/sessions/runtime-types.ts: UpdateLastRoute",
|
||||
"src/config/sessions/session-accessor.sqlite.ts: appendSqliteTranscriptEvents",
|
||||
"src/config/sessions/session-accessor.sqlite.ts: deleteSqliteTranscript",
|
||||
"src/config/sessions/session-accessor.sqlite.ts: sqliteTranscriptExists",
|
||||
"src/config/sessions/session-registry-maintenance.ts: SessionRegistryMaintenanceStoreOptions",
|
||||
"src/config/sessions/session-registry-maintenance.ts: SessionRegistryMaintenanceStoreSummary",
|
||||
"src/config/sessions/session-transcript-index.ts: extractTranscriptIndexEntry",
|
||||
"src/config/sessions/session-transcript-search.ts: resetSessionTranscriptSearchForTest",
|
||||
"src/config/sessions/session-transcript-search.ts: waitForSessionTranscriptReconcileForTest",
|
||||
"src/config/sessions/skill-prompt-blobs.ts: getSessionSkillPromptRefCacheStatsForTest",
|
||||
"src/config/sessions/skill-prompt-blobs.ts: getValidSessionSkillPromptBlobCacheStatsForTest",
|
||||
"src/config/sessions/store-cache.ts: DeepReadonly",
|
||||
"src/config/sessions/store-cache.ts: getSerializedSessionStoreCacheStatsForTest",
|
||||
"src/config/sessions/store-cache.ts: getSessionStoreSnapshotCacheStatsForTest",
|
||||
"src/config/sessions/store-cache.ts: getSessionStoreStringInternStatsForTest",
|
||||
"src/config/sessions/store-load.ts: LoadSessionStoreOptions",
|
||||
"src/config/sessions/store-load.ts: ReadSessionEntryOptions",
|
||||
"src/config/sessions/store-load.ts: readSessionStoreSnapshot",
|
||||
"src/config/sessions/store-maintenance-operations.ts: FileBackedSessionStoreMaintenanceParams",
|
||||
"src/config/sessions/store-maintenance-operations.ts: FileBackedSessionStoreMaintenanceResult",
|
||||
"src/config/sessions/store-maintenance-preserve.ts: SessionMaintenancePreserveKeysProvider",
|
||||
"src/config/sessions/store-maintenance.ts: isGatewayModelRunSessionKey",
|
||||
"src/config/sessions/store-maintenance.ts: isProtectedSessionMaintenanceEntry",
|
||||
"src/config/sessions/store-maintenance.ts: QuotaSuspensionEntryMaintenanceResult",
|
||||
"src/config/sessions/store-maintenance.ts: resolveSessionEntryMaintenanceHighWater",
|
||||
"src/config/sessions/store-writer-state.ts: getSessionStoreWriterQueueSizeForTest",
|
||||
"src/config/sessions/store-writer-state.ts: SessionStoreWriterQueue",
|
||||
"src/config/sessions/store-writer.ts: RunExclusiveSessionStoreWriteOptions",
|
||||
"src/config/sessions/store.ts: applySessionEntryLifecycleMutation",
|
||||
"src/config/sessions/store.ts: archiveRemovedSessionTranscripts",
|
||||
"src/config/sessions/store.ts: capEntryCount",
|
||||
"src/config/sessions/store.ts: cleanupSessionLifecycleArtifacts",
|
||||
"src/config/sessions/store.ts: drainSessionStoreWriterQueuesForTest",
|
||||
"src/config/sessions/store.ts: getActiveSessionMaintenanceWarning",
|
||||
"src/config/sessions/store.ts: getSessionStoreWriterQueueSizeForTest",
|
||||
"src/config/sessions/store.ts: listSessionEntries",
|
||||
"src/config/sessions/store.ts: normalizeStoreSessionKey",
|
||||
"src/config/sessions/store.ts: patchSessionEntry",
|
||||
"src/config/sessions/store.ts: pruneStaleModelRunEntries",
|
||||
"src/config/sessions/store.ts: purgeDeletedAgentSessionEntries",
|
||||
"src/config/sessions/store.ts: readSessionEntries",
|
||||
"src/config/sessions/store.ts: readSessionStoreSnapshot",
|
||||
"src/config/sessions/store.ts: recordSessionMetaFromInbound",
|
||||
"src/config/sessions/store.ts: ResolvedSessionMaintenanceConfig",
|
||||
"src/config/sessions/store.ts: resolveMaintenanceConfig",
|
||||
"src/config/sessions/store.ts: rollbackAgentHarnessSessionEntryLifecycle",
|
||||
"src/config/sessions/store.ts: rollbackPluginOwnedSessionEntryLifecycle",
|
||||
"src/config/sessions/store.ts: SessionMaintenanceApplyReport",
|
||||
"src/config/sessions/store.ts: SessionMaintenanceWarning",
|
||||
"src/config/sessions/store.ts: SessionStoreSnapshot",
|
||||
"src/config/sessions/store.ts: SessionStoreSnapshotEntries",
|
||||
"src/config/sessions/store.ts: SessionStoreSnapshotEntry",
|
||||
"src/config/sessions/store.ts: updateLastRoute",
|
||||
"src/config/sessions/store.ts: upsertSessionEntry",
|
||||
"src/config/sessions/transcript-jsonl.ts: appendJsonlEntry",
|
||||
"src/config/sessions/transcript-jsonl.ts: appendSerializedJsonlEntry",
|
||||
"src/config/sessions/transcript-jsonl.ts: writeJsonlEntry",
|
||||
"src/config/sessions/transcript-replay.ts: DEFAULT_REPLAY_MAX_MESSAGES",
|
||||
"src/config/sessions/transcript-stream.ts: TranscriptReverseStreamOptions",
|
||||
"src/config/sessions/transcript-stream.ts: TranscriptStreamOptions",
|
||||
"src/config/sessions/transcript-tree.ts: SessionTranscriptTreeEntry",
|
||||
"src/config/sessions/transcript-tree.ts: SessionTranscriptTreeNode",
|
||||
"src/config/sessions/transcript-write-context.ts: resolveOwnedSessionTranscriptWriteLockRunner",
|
||||
"src/config/sessions/transcript-write-context.ts: runWithOwnedSessionTranscriptWritePublication",
|
||||
"src/config/sessions/transcript.ts: ReadRecentSessionConversationTextOptions",
|
||||
"src/config/sessions/transcript.ts: ReadRecentSessionConversationTextParams",
|
||||
"src/config/sessions/transcript.ts: readRecentUserAssistantTextFromSessionTranscript",
|
||||
"src/config/sessions/transcript.ts: TailAssistantTranscriptText",
|
||||
"src/config/sessions/types.ts: LaneExecutionState",
|
||||
"src/config/sessions/types.ts: mergeSessionEntryWithPolicy",
|
||||
"src/config/sessions/types.ts: SessionAcpIdentitySource",
|
||||
"src/config/sessions/types.ts: SessionAcpIdentityState",
|
||||
"src/config/sessions/types.ts: SessionChannelId",
|
||||
"src/config/sessions/types.ts: SessionCompactionTranscriptReference",
|
||||
"src/config/sessions/types.ts: SessionContextBudgetStatusRoute",
|
||||
"src/config/sessions/types.ts: SessionEntryMergePolicy",
|
||||
"src/config/sessions/types.ts: SessionPluginDebugEntry",
|
||||
"src/config/sessions/types.ts: SessionPluginJsonValue",
|
||||
"src/config/sessions/types.ts: SessionPluginNextTurnInjection",
|
||||
"src/config/sessions/types.ts: SubagentRecoveryState",
|
||||
"src/config/state-dir-dotenv.ts: DurableServiceEnvVarSources",
|
||||
"src/config/state-dir-dotenv.ts: readStateDirDotEnvVars",
|
||||
"src/config/validation.ts: __testing",
|
||||
"src/config/validation.ts: testing",
|
||||
"src/config/version.ts: isSameOpenClawStableFamily",
|
||||
"src/config/version.ts: parseOpenClawVersion",
|
||||
"src/config/zod-schema.agent-runtime.ts: AgentRuntimePolicySchema",
|
||||
"src/config/zod-schema.agents.ts: BroadcastStrategySchema",
|
||||
"src/config/zod-schema.core.ts: _ModelCompatSchemaAssignableToType",
|
||||
"src/config/zod-schema.core.ts: _ModelCompatTypeAssignableToSchema",
|
||||
"src/config/zod-schema.core.ts: _ToolsMediaAsyncCompletionSchemaAssignableToType",
|
||||
"src/config/zod-schema.core.ts: _ToolsMediaAsyncCompletionTypeAssignableToSchema",
|
||||
"src/config/zod-schema.core.ts: MarkdownTableModeSchema",
|
||||
"src/config/zod-schema.core.ts: MentionPatternsModeSchema",
|
||||
"src/config/zod-schema.core.ts: normalizeAllowFrom",
|
||||
"src/config/zod-schema.providers-core.ts: DiscordAccountSchema",
|
||||
"src/config/zod-schema.providers-core.ts: DiscordDmSchema",
|
||||
"src/config/zod-schema.providers-core.ts: DiscordGuildChannelSchema",
|
||||
"src/config/zod-schema.providers-core.ts: DiscordGuildSchema",
|
||||
"src/config/zod-schema.providers-core.ts: DiscordThreadSchema",
|
||||
"src/config/zod-schema.providers-core.ts: IMessageAccountSchemaBase",
|
||||
"src/config/zod-schema.providers-core.ts: IrcAccountSchema",
|
||||
"src/config/zod-schema.providers-core.ts: IrcAccountSchemaBase",
|
||||
"src/config/zod-schema.providers-core.ts: IrcConfigSchema",
|
||||
"src/config/zod-schema.providers-core.ts: IrcGroupSchema",
|
||||
"src/config/zod-schema.providers-core.ts: IrcNickServSchema",
|
||||
"src/config/zod-schema.providers-core.ts: MSTeamsChannelSchema",
|
||||
"src/config/zod-schema.providers-core.ts: MSTeamsTeamSchema",
|
||||
"src/config/zod-schema.providers-core.ts: SignalAccountSchemaBase",
|
||||
"src/config/zod-schema.providers-core.ts: SlackAccountSchema",
|
||||
"src/config/zod-schema.providers-core.ts: SlackChannelSchema",
|
||||
"src/config/zod-schema.providers-core.ts: SlackDmSchema",
|
||||
"src/config/zod-schema.providers-core.ts: SlackRelaySchema",
|
||||
"src/config/zod-schema.providers-core.ts: SlackSocketModeSchema",
|
||||
"src/config/zod-schema.providers-core.ts: SlackThreadSchema",
|
||||
"src/config/zod-schema.providers-core.ts: TelegramAccountSchema",
|
||||
"src/config/zod-schema.providers-core.ts: TelegramAccountSchemaBase",
|
||||
"src/config/zod-schema.providers-core.ts: TelegramDirectSchema",
|
||||
"src/config/zod-schema.providers-core.ts: TelegramGroupSchema",
|
||||
"src/config/zod-schema.providers-core.ts: TelegramTopicSchema",
|
||||
"src/config/zod-schema.providers-googlechat.ts: GoogleChatAccountSchema",
|
||||
"src/config/zod-schema.providers-googlechat.ts: GoogleChatDmSchema",
|
||||
"src/config/zod-schema.providers-googlechat.ts: GoogleChatGroupSchema",
|
||||
"src/config/zod-schema.providers-whatsapp.ts: WhatsAppAccountSchema",
|
||||
"src/config/zod-schema.proxy.ts: ProxyLoopbackModeSchema",
|
||||
"src/config/zod-schema.ts: installZodDefaultLocale",
|
||||
"src/config/zod-schema.ts: McpServerSchema",
|
||||
"src/context-engine/registry.ts: clearContextEngineRuntimeQuarantine",
|
||||
"src/context-engine/registry.ts: ContextEngineRegistrationResult",
|
||||
"src/context-engine/registry.ts: getContextEngineFactory",
|
||||
@@ -1606,345 +1398,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
|
||||
"src/image-generation/runtime-types.ts: ListRuntimeImageGenerationProvidersParams",
|
||||
"src/image-generation/runtime-types.ts: RuntimeImageGenerationProvider",
|
||||
"src/image-generation/runtime.ts: ImageGenerationRuntimeDeps",
|
||||
"src/infra/active-node-context.ts: ActiveNodeContext",
|
||||
"src/infra/active-node-context.ts: resetActiveNodeContextForTests",
|
||||
"src/infra/agent-events.ts: AgentApprovalEventKind",
|
||||
"src/infra/agent-events.ts: AgentApprovalEventPhase",
|
||||
"src/infra/agent-events.ts: AgentApprovalEventStatus",
|
||||
"src/infra/agent-events.ts: AgentItemEventKind",
|
||||
"src/infra/agent-events.ts: AgentItemEventPhase",
|
||||
"src/infra/agent-events.ts: AgentItemEventStatus",
|
||||
"src/infra/agent-events.ts: AgentRunContext",
|
||||
"src/infra/agent-events.ts: resetAgentRunContextForTest",
|
||||
"src/infra/approval-native-route-coordinator.ts: clearApprovalNativeRouteStateForTest",
|
||||
"src/infra/approval-resolution-ref.ts: APPROVAL_RESOLUTION_REF_LENGTH",
|
||||
"src/infra/approval-view-model.ts: isTypedApprovalActionView",
|
||||
"src/infra/archive.ts: ArchiveSecurityError",
|
||||
"src/infra/backup-create.ts: __test",
|
||||
"src/infra/backup-create.ts: buildExtensionsNodeModulesFilter",
|
||||
"src/infra/backup-create.ts: testApi",
|
||||
"src/infra/clawhub-install-trust.ts: ClawHubTrustAcceptedResult",
|
||||
"src/infra/clawhub-install-trust.ts: ClawHubTrustFailure",
|
||||
"src/infra/clawhub-install-trust.ts: ClawHubTrustInstallRecordFields",
|
||||
"src/infra/clawhub.ts: ClawHubArtifactModerationState",
|
||||
"src/infra/clawhub.ts: ClawHubArtifactScanState",
|
||||
"src/infra/clawhub.ts: ClawHubPackageEnvironmentSummary",
|
||||
"src/infra/clawhub.ts: ClawHubPackageHostTarget",
|
||||
"src/infra/clawhub.ts: ClawHubPackageListItem",
|
||||
"src/infra/clawhub.ts: ClawHubPromotionModel",
|
||||
"src/infra/clawhub.ts: ClawHubPromotionsFeed",
|
||||
"src/infra/clawhub.ts: ClawHubPromotionsFeedFetchResult",
|
||||
"src/infra/clawhub.ts: ClawHubSkillSecurityVerdictRequestItem",
|
||||
"src/infra/clawhub.ts: ClawHubSkillSecurityVerdictsResponse",
|
||||
"src/infra/clawhub.ts: ClawHubSkillVerificationDecision",
|
||||
"src/infra/clawhub.ts: parseClawHubPromotion",
|
||||
"src/infra/clawhub.ts: resolveClawHubAuthToken",
|
||||
"src/infra/command-analysis/explain.ts: explainCommandForDisplay",
|
||||
"src/infra/command-analysis/explain.ts: summarizeCommandExplanation",
|
||||
"src/infra/command-analysis/explain.ts: summarizeCommandSegmentsForDisplay",
|
||||
"src/infra/command-analysis/policy.ts: CommandPolicyAnalysis",
|
||||
"src/infra/command-analysis/risks.ts: CarriedShellBuiltinHit",
|
||||
"src/infra/command-analysis/risks.ts: COMMAND_CARRIER_EXECUTABLES",
|
||||
"src/infra/command-analysis/risks.ts: CommandCarrierHit",
|
||||
"src/infra/command-analysis/risks.ts: detectEnvSplitStringFlag",
|
||||
"src/infra/command-analysis/risks.ts: resolveCarrierCommandArgv",
|
||||
"src/infra/command-carriers.ts: ParsedEnvInvocationPrelude",
|
||||
"src/infra/command-explainer/tree-sitter-runtime.ts: getBashParserForCommandExplanation",
|
||||
"src/infra/command-explainer/tree-sitter-runtime.ts: resolvePackageFileForCommandExplanation",
|
||||
"src/infra/command-explainer/tree-sitter-runtime.ts: setBashParserLoaderForCommandExplanationForTest",
|
||||
"src/infra/control-ui-assets.ts: resolveControlUiDistIndexPath",
|
||||
"src/infra/control-ui-assets.ts: resolveControlUiRepoRoot",
|
||||
"src/infra/delivery-queue-sqlite.ts: DeliveryQueueEntryState",
|
||||
"src/infra/delivery-queue-sqlite.ts: FailedDeliveryQueueCount",
|
||||
"src/infra/delivery-queue-sqlite.ts: FailPendingDeliveryQueueEntryResult",
|
||||
"src/infra/delivery-recovery.shared.ts: RECOVERY_REPLAY_SPACING_MS",
|
||||
"src/infra/dev-install-branch.ts: detectDevInstallGitBranch",
|
||||
"src/infra/device-bootstrap.ts: DEVICE_BOOTSTRAP_TOKEN_TTL_MS",
|
||||
"src/infra/device-bootstrap.ts: DeviceBootstrapTokenRecord",
|
||||
"src/infra/device-pairing-migration.ts: LegacyDevicePairingMigrationResult",
|
||||
"src/infra/device-pairing-store.ts: DevicePairingStoreState",
|
||||
"src/infra/device-pairing-store.ts: DevicePairingStoreTarget",
|
||||
"src/infra/device-pairing-store.ts: resolveDevicePairingStateDbOptions",
|
||||
"src/infra/device-pairing.ts: ApproveDevicePairingResult",
|
||||
"src/infra/device-pairing.ts: DevicePairingAccessMetadata",
|
||||
"src/infra/device-pairing.ts: DevicePairingForbiddenReason",
|
||||
"src/infra/device-pairing.ts: DevicePairingForbiddenResult",
|
||||
"src/infra/device-pairing.ts: DevicePairingList",
|
||||
"src/infra/device-pairing.ts: DevicePairingPendingRecord",
|
||||
"src/infra/device-pairing.ts: DevicePairingSupersededRequest",
|
||||
"src/infra/device-pairing.ts: PairedDeviceApprovalKind",
|
||||
"src/infra/device-pairing.ts: PairedDeviceMetadataPatch",
|
||||
"src/infra/device-pairing.ts: PairedDeviceNodeSurface",
|
||||
"src/infra/device-pairing.ts: RequestDevicePairingResult",
|
||||
"src/infra/device-pairing.ts: RevokeDeviceTokenResult",
|
||||
"src/infra/device-pairing.ts: RotateDeviceTokenResult",
|
||||
"src/infra/diagnostic-trace-context.ts: resetDiagnosticTraceContextForTest",
|
||||
"src/infra/dispatch-wrapper-resolution.ts: unwrapEnvInvocation",
|
||||
"src/infra/ed25519-signature.ts: base64UrlEncode",
|
||||
"src/infra/event-session-routing.ts: parseDirectAgentSessionTarget",
|
||||
"src/infra/exec-approval-forwarder.ts: buildExecApprovalRequestMessage",
|
||||
"src/infra/exec-approval-policy-snapshot.ts: ExecApprovalPolicyRule",
|
||||
"src/infra/exec-approvals-effective.ts: ExecPolicyHostDefaults",
|
||||
"src/infra/exec-approvals.ts: DurableExecApprovalRequirement",
|
||||
"src/infra/exec-approvals.ts: persistAllowAlwaysDecisionLocked",
|
||||
"src/infra/exec-approvals.ts: recordAllowlistMatchesUseLocked",
|
||||
"src/infra/exec-authorization-plan.ts: ExecAuthorizationDialect",
|
||||
"src/infra/exec-authorization-plan.ts: ExecAuthorizationGroup",
|
||||
"src/infra/exec-authorization-plan.ts: ExecAuthorizationTransport",
|
||||
"src/infra/exec-authorization-plan.ts: ExecAuthorizationTrustMode",
|
||||
"src/infra/exec-authorization-render.ts: AuthorizedShellRenderMode",
|
||||
"src/infra/exec-authorization-render.ts: AuthorizedShellRenderResult",
|
||||
"src/infra/exec-auto-review.ts: ExecAutoReviewHost",
|
||||
"src/infra/exec-control-command-guard.ts: UnsafeExecControlShellCommandKind",
|
||||
"src/infra/exec-policy.ts: ExecPolicyLayer",
|
||||
"src/infra/exec-safe-bin-policy-profiles.ts: renderDefaultSafeBinsDocText",
|
||||
"src/infra/exec-safe-bin-policy-profiles.ts: renderSafeBinDeniedFlagsDocBullets",
|
||||
"src/infra/exec-safe-bin-policy-profiles.ts: SAFE_BIN_PROFILE_FIXTURES",
|
||||
"src/infra/exec-safe-bin-policy.ts: buildLongFlagPrefixMap",
|
||||
"src/infra/exec-safe-bin-policy.ts: collectKnownLongFlags",
|
||||
"src/infra/exec-safe-bin-policy.ts: renderDefaultSafeBinsDocText",
|
||||
"src/infra/exec-safe-bin-policy.ts: renderSafeBinDeniedFlagsDocBullets",
|
||||
"src/infra/exec-safe-bin-policy.ts: SAFE_BIN_PROFILE_FIXTURES",
|
||||
"src/infra/exec-wrapper-resolution.ts: basenameLower",
|
||||
"src/infra/exec-wrapper-resolution.ts: extractShellWrapperInlineCommand",
|
||||
"src/infra/exec-wrapper-resolution.ts: isDispatchWrapperExecutable",
|
||||
"src/infra/exec-wrapper-resolution.ts: resolveDispatchWrapperTrustPlan",
|
||||
"src/infra/exec-wrapper-resolution.ts: unwrapEnvInvocation",
|
||||
"src/infra/exec-wrapper-tokens.ts: basenameLower",
|
||||
"src/infra/executable-path.ts: isExecutableFile",
|
||||
"src/infra/file-store.ts: FileStore",
|
||||
"src/infra/fs-safe-advanced.ts: assertNoHardlinkedFinalPath",
|
||||
"src/infra/fs-safe-advanced.ts: FileIdentityStat",
|
||||
"src/infra/fs-safe-advanced.ts: writeSiblingTempFile",
|
||||
"src/infra/gateway-active-work.ts: GatewayActiveWorkCounts",
|
||||
"src/infra/gateway-boot-lifecycle.ts: GATEWAY_BOOT_LIFECYCLE_RETENTION_MS",
|
||||
"src/infra/gateway-boot-lifecycle.ts: GATEWAY_BOOT_LOOP_UNCLEAN_THRESHOLD",
|
||||
"src/infra/gateway-boot-lifecycle.ts: GATEWAY_BOOT_LOOP_WINDOW_MS",
|
||||
"src/infra/gateway-boot-lifecycle.ts: GatewayBootLifecycleOutcome",
|
||||
"src/infra/gateway-suspend-coordinator.ts: GATEWAY_SUSPEND_RETRY_AFTER_MS",
|
||||
"src/infra/gateway-suspend-coordinator.ts: GATEWAY_SUSPEND_TTL_MS",
|
||||
"src/infra/gateway-suspend-coordinator.ts: GatewaySuspendPrepareResult",
|
||||
"src/infra/gateway-suspend-coordinator.ts: GatewaySuspendResumeResult",
|
||||
"src/infra/gateway-suspend-coordinator.ts: GatewaySuspendStatusResult",
|
||||
"src/infra/gateway-suspend-coordinator.ts: resetGatewaySuspendCoordinatorForTest",
|
||||
"src/infra/git-commit.ts: CommitMetadataReaders",
|
||||
"src/infra/git-commit.ts: testing",
|
||||
"src/infra/heartbeat-cooldown.ts: DEFAULT_FLOOD_THRESHOLD",
|
||||
"src/infra/heartbeat-cooldown.ts: DEFAULT_MIN_WAKE_SPACING_MS",
|
||||
"src/infra/heartbeat-cooldown.ts: ShouldDeferInput",
|
||||
"src/infra/heartbeat-wake.ts: hasHeartbeatWakeHandler",
|
||||
"src/infra/heartbeat-wake.ts: hasPendingHeartbeatWake",
|
||||
"src/infra/heartbeat-wake.ts: HeartbeatWakeOverride",
|
||||
"src/infra/heartbeat-wake.ts: resetHeartbeatWakeStateForTests",
|
||||
"src/infra/heartbeat-wake.ts: RetryableHeartbeatBusySkipReason",
|
||||
"src/infra/inline-option-token.ts: InlineOptionToken",
|
||||
"src/infra/install-source-utils.ts: NpmResolutionFields",
|
||||
"src/infra/jsonl-socket.ts: testApi",
|
||||
"src/infra/net/http-connect-tunnel.ts: HttpConnectTunnelParams",
|
||||
"src/infra/net/proxy/active-proxy-state.ts: resetActiveManagedProxyStateForTests",
|
||||
"src/infra/net/proxy/proxy-validation.ts: DEFAULT_PROXY_VALIDATION_ALLOWED_URLS",
|
||||
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationApnsCheck",
|
||||
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationApnsCheckParams",
|
||||
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationApnsCheckResult",
|
||||
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationCheck",
|
||||
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationCheckKind",
|
||||
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationConfigSource",
|
||||
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationFetchCheck",
|
||||
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationFetchCheckParams",
|
||||
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationFetchCheckResult",
|
||||
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationResolvedConfig",
|
||||
"src/infra/net/proxy/proxy-validation.ts: resolveProxyValidationConfig",
|
||||
"src/infra/net/proxy/proxy-validation.ts: ResolveProxyValidationConfigOptions",
|
||||
"src/infra/net/proxy/proxy-validation.ts: RunProxyValidationOptions",
|
||||
"src/infra/net/undici-runtime.ts: TEST_UNDICI_RUNTIME_DEPS_KEY",
|
||||
"src/infra/node-pairing-migration.ts: LegacyNodePairingMigrationResult",
|
||||
"src/infra/node-pairing.ts: NodePairingPendingSnapshot",
|
||||
"src/infra/npm-integrity.ts: resolveNpmIntegrityDrift",
|
||||
"src/infra/npm-managed-root.ts: MissingRequiredPlatformPackage",
|
||||
"src/infra/npm-pack-install.ts: installFromNpmSpecArchive",
|
||||
"src/infra/npm-registry-spec.ts: isOpenClawStableCorrectionVersion",
|
||||
"src/infra/openclaw-exec-env.ts: OPENCLAW_CLI_ENV_VALUE",
|
||||
"src/infra/openclaw-root.ts: __testing",
|
||||
"src/infra/openclaw-root.ts: testing",
|
||||
"src/infra/outbound/channel-resolution.ts: resetOutboundChannelResolutionStateForTest",
|
||||
"src/infra/outbound/channel-selection.ts: testing",
|
||||
"src/infra/outbound/deliver-types.ts: PLATFORM_MESSAGE_NOT_DISPATCHED_ERROR_CODE",
|
||||
"src/infra/outbound/deliver.ts: normalizeOutboundPayloads",
|
||||
"src/infra/outbound/delivery-queue-recovery.ts: computeBackoffMs",
|
||||
"src/infra/outbound/delivery-queue-recovery.ts: isEntryEligibleForRecoveryRetry",
|
||||
"src/infra/outbound/delivery-queue-recovery.ts: isPermanentDeliveryError",
|
||||
"src/infra/outbound/delivery-queue-recovery.ts: MAX_RETRIES",
|
||||
"src/infra/outbound/delivery-queue.ts: computeBackoffMs",
|
||||
"src/infra/outbound/delivery-queue.ts: failPendingDelivery",
|
||||
"src/infra/outbound/delivery-queue.ts: isEntryEligibleForRecoveryRetry",
|
||||
"src/infra/outbound/delivery-queue.ts: isPermanentDeliveryError",
|
||||
"src/infra/outbound/delivery-queue.ts: loadPendingDeliveries",
|
||||
"src/infra/outbound/delivery-queue.ts: loadPendingDelivery",
|
||||
"src/infra/outbound/delivery-queue.ts: MAX_RETRIES",
|
||||
"src/infra/outbound/delivery-queue.ts: moveToFailed",
|
||||
"src/infra/outbound/directory-cache.ts: DirectoryCacheKey",
|
||||
"src/infra/outbound/payloads.ts: normalizeOutboundPayloads",
|
||||
"src/infra/outbound/target-errors.ts: ambiguousTargetMessage",
|
||||
"src/infra/outbound/target-errors.ts: missingTargetMessage",
|
||||
"src/infra/outbound/target-errors.ts: unknownTargetMessage",
|
||||
"src/infra/outbound/target-normalization.ts: testing",
|
||||
"src/infra/outbound/target-resolver.ts: resolveMessagingTarget",
|
||||
"src/infra/package-dist-inventory.ts: assertNoLegacyPluginDependencyStagingDebris",
|
||||
"src/infra/package-dist-inventory.ts: collectLegacyPluginDependencyStagingDebrisPaths",
|
||||
"src/infra/package-dist-inventory.ts: isLegacyPluginDependencyInstallStagePath",
|
||||
"src/infra/package-dist-inventory.ts: LOCAL_BUILD_METADATA_DIST_PATHS",
|
||||
"src/infra/package-dist-inventory.ts: writePackageDistInventory",
|
||||
"src/infra/package-json.ts: readPackageJson",
|
||||
"src/infra/package-manager-exec-wrapper.ts: NPM_EXEC_OPTIONS_WITH_VALUE",
|
||||
"src/infra/package-manager-exec-wrapper.ts: PackageManagerExecInvocation",
|
||||
"src/infra/package-update-steps.ts: PackageUpdateStepResult",
|
||||
"src/infra/pairing-files.ts: writeJson",
|
||||
"src/infra/pairing-token.ts: PAIRING_TOKEN_BYTES",
|
||||
"src/infra/path-guards.ts: hasNodeErrorCode",
|
||||
"src/infra/path-guards.ts: isNodeError",
|
||||
"src/infra/path-guards.ts: isPathInsideWithRealpath",
|
||||
"src/infra/path-guards.ts: isSymlinkOpenError",
|
||||
"src/infra/path-guards.ts: isWithinDir",
|
||||
"src/infra/path-guards.ts: resolveSafeBaseDir",
|
||||
"src/infra/path-guards.ts: resolveSafeRelativePath",
|
||||
"src/infra/path-guards.ts: safeRealpathSync",
|
||||
"src/infra/path-guards.ts: splitSafeRelativePath",
|
||||
"src/infra/path-safety.ts: hasNodeErrorCode",
|
||||
"src/infra/path-safety.ts: isNodeError",
|
||||
"src/infra/path-safety.ts: isNotFoundPathError",
|
||||
"src/infra/path-safety.ts: isSymlinkOpenError",
|
||||
"src/infra/path-safety.ts: normalizeWindowsPathForComparison",
|
||||
"src/infra/path-safety.ts: resolveSafeBaseDir",
|
||||
"src/infra/path-safety.ts: resolveSafeRelativePath",
|
||||
"src/infra/path-safety.ts: splitSafeRelativePath",
|
||||
"src/infra/permissions.ts: formatWindowsAclSummary",
|
||||
"src/infra/permissions.ts: inspectWindowsAcl",
|
||||
"src/infra/permissions.ts: parseIcaclsOutput",
|
||||
"src/infra/permissions.ts: resolveWindowsUserPrincipal",
|
||||
"src/infra/permissions.ts: summarizeWindowsAcl",
|
||||
"src/infra/permissions.ts: WindowsAclEntry",
|
||||
"src/infra/permissions.ts: WindowsAclSummary",
|
||||
"src/infra/ports-format.ts: formatPortListener",
|
||||
"src/infra/ports-format.ts: isSingleExpectedGatewayListener",
|
||||
"src/infra/ports-netstat.ts: WindowsNetstatListener",
|
||||
"src/infra/ports.ts: buildPortHints",
|
||||
"src/infra/ports.ts: isSingleExpectedGatewayListener",
|
||||
"src/infra/ports.ts: PortConnections",
|
||||
"src/infra/ports.ts: PortListenerKind",
|
||||
"src/infra/private-file-store.ts: PrivateFileStoreSync",
|
||||
"src/infra/promotions-feed.ts: PromotionClaimRecord",
|
||||
"src/infra/promotions-feed.ts: PromotionsFeedState",
|
||||
"src/infra/promotions-feed.ts: readPromotionsFeedState",
|
||||
"src/infra/push-apns-http2.ts: ApnsResponseBodyCapture",
|
||||
"src/infra/push-apns-http2.ts: ConnectApnsHttp2SessionParams",
|
||||
"src/infra/push-apns-http2.ts: ProbeApnsHttp2ReachabilityViaProxyParams",
|
||||
"src/infra/push-apns-http2.ts: ProbeApnsHttp2ReachabilityViaProxyResult",
|
||||
"src/infra/push-apns.relay.ts: DEFAULT_APNS_RELAY_BASE_URL",
|
||||
"src/infra/push-apns.relay.ts: DEFAULT_APNS_SANDBOX_RELAY_BASE_URL",
|
||||
"src/infra/push-apns.ts: ApnsPushResult",
|
||||
"src/infra/push-apns.ts: shouldInvalidateApnsRegistration",
|
||||
"src/infra/push-web.ts: listWebPushSubscriptions",
|
||||
"src/infra/regular-file.ts: RegularFileStatResult",
|
||||
"src/infra/regular-file.ts: resolveRegularFileAppendFlags",
|
||||
"src/infra/regular-file.ts: statRegularFile",
|
||||
"src/infra/regular-file.ts: statRegularFileSync",
|
||||
"src/infra/resolve-system-bin.ts: getTrustedDirsForTest",
|
||||
"src/infra/resolve-system-bin.ts: resetResolveSystemBin",
|
||||
"src/infra/restart-coordinator.ts: SafeGatewayRestartBlocker",
|
||||
"src/infra/restart-coordinator.ts: SafeGatewayRestartCounts",
|
||||
"src/infra/restart-coordinator.ts: SafeGatewayRestartPreflight",
|
||||
"src/infra/restart-handoff.ts: GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND",
|
||||
"src/infra/restart-handoff.ts: GatewayRestartHandoffRestartKind",
|
||||
"src/infra/restart-handoff.ts: GatewayRestartHandoffSource",
|
||||
"src/infra/restart-handoff.ts: GatewayRestartHandoffSupervisorMode",
|
||||
"src/infra/restart-sentinel.ts: RestartSentinel",
|
||||
"src/infra/restart-sentinel.ts: RestartSentinelLog",
|
||||
"src/infra/restart-sentinel.ts: RestartSentinelStats",
|
||||
"src/infra/restart-sentinel.ts: RestartSentinelStep",
|
||||
"src/infra/restart-stale-pids.ts: __testing",
|
||||
"src/infra/restart-stale-pids.ts: testing",
|
||||
"src/infra/restart.ts: __testing",
|
||||
"src/infra/restart.ts: DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS",
|
||||
"src/infra/restart.ts: emitGatewayRestart",
|
||||
"src/infra/restart.ts: findGatewayPidsOnPortSync",
|
||||
"src/infra/restart.ts: GatewayRestartEmitResult",
|
||||
"src/infra/restart.ts: RestartAttempt",
|
||||
"src/infra/restart.ts: RestartAuditInfo",
|
||||
"src/infra/restart.ts: RestartDeferralHooks",
|
||||
"src/infra/restart.ts: RestartEmitHooks",
|
||||
"src/infra/restart.ts: testing",
|
||||
"src/infra/root-paths.ts: ensureDirectoryWithinRoot",
|
||||
"src/infra/runtime-guard.ts: detectRuntime",
|
||||
"src/infra/runtime-guard.ts: parseMinimumNodeEngine",
|
||||
"src/infra/runtime-guard.ts: RuntimeDetails",
|
||||
"src/infra/runtime-guard.ts: runtimeSatisfies",
|
||||
"src/infra/session-cost-usage-cache.sqlite.ts: sessionCostUsageCacheTestApi",
|
||||
"src/infra/session-cost-usage.ts: loadSessionCostSummaryFromCache",
|
||||
"src/infra/session-cost-usage.ts: refreshCostUsageCache",
|
||||
"src/infra/session-cost-usage.ts: requestCostUsageCacheRefresh",
|
||||
"src/infra/session-delivery-queue-recovery.ts: isSessionDeliveryEligibleForRetry",
|
||||
"src/infra/session-delivery-queue.ts: ackSessionDelivery",
|
||||
"src/infra/session-delivery-queue.ts: failSessionDelivery",
|
||||
"src/infra/session-delivery-queue.ts: isSessionDeliveryEligibleForRetry",
|
||||
"src/infra/session-delivery-queue.ts: loadPendingSessionDeliveries",
|
||||
"src/infra/session-maintenance-warning.ts: __testing",
|
||||
"src/infra/session-maintenance-warning.ts: testing",
|
||||
"src/infra/shell-env.ts: resetShellPathCacheForTests",
|
||||
"src/infra/shell-inline-command.ts: POWERSHELL_INLINE_COMMAND_FLAGS",
|
||||
"src/infra/sqlite-integrity.ts: SqliteIntegrityChecks",
|
||||
"src/infra/sqlite-snapshot.ts: CreateVerifiedSqliteSnapshotOptions",
|
||||
"src/infra/sqlite-snapshot.ts: PublishedSqliteFileGuard",
|
||||
"src/infra/sqlite-snapshot.ts: PublishVerifiedSqliteFileOptions",
|
||||
"src/infra/sqlite-snapshot.ts: SqliteFileContent",
|
||||
"src/infra/sqlite-snapshot.ts: VerifiedSqliteSnapshot",
|
||||
"src/infra/sqlite-wal.ts: DEFAULT_SQLITE_WAL_AUTOCHECKPOINT_PAGES",
|
||||
"src/infra/sqlite-wal.ts: DEFAULT_SQLITE_WAL_CHECKPOINT_INTERVAL_MS",
|
||||
"src/infra/sqlite-wal.ts: enableIncrementalAutoVacuumForFreshDatabase",
|
||||
"src/infra/stale-lock-file.ts: LockFileOwnerPayload",
|
||||
"src/infra/state-migrations.cron-run-logs.ts: CRON_RUN_LOG_TASK_IMPORT_MIGRATION_ID",
|
||||
"src/infra/state-migrations.cron-run-logs.ts: CronRunLogTaskImportResult",
|
||||
"src/infra/state-migrations.debug-proxy.ts: LegacyDebugProxyCaptureDetection",
|
||||
"src/infra/state-migrations.fs.ts: isLegacyWhatsAppAuthFile",
|
||||
"src/infra/state-migrations.session-store.ts: sessionStoreTextMayNeedCanonicalization",
|
||||
"src/infra/state-migrations.ts: sessionStoreTextMayNeedCanonicalization",
|
||||
"src/infra/supervisor-markers.ts: DetectRespawnSupervisorOptions",
|
||||
"src/infra/system-run-approval-binding.ts: matchSystemRunApprovalEnvHash",
|
||||
"src/infra/system-run-command.ts: validateSystemRunCommandConsistency",
|
||||
"src/infra/tailnet.ts: listTailnetAddresses",
|
||||
"src/infra/tailscale.ts: getTestTailscaleBinaryOverride",
|
||||
"src/infra/tailscale.ts: tailscaleFunnelStatusCoversPort",
|
||||
"src/infra/update-channels.ts: isPrereleaseTag",
|
||||
"src/infra/update-channels.ts: UpdateChannelSource",
|
||||
"src/infra/update-check.ts: checkDepsStatus",
|
||||
"src/infra/update-check.ts: DepsStatus",
|
||||
"src/infra/update-check.ts: ExtendedStableResolutionResult",
|
||||
"src/infra/update-check.ts: fetchNpmLatestVersion",
|
||||
"src/infra/update-check.ts: fetchNpmRegistryVersionForChannel",
|
||||
"src/infra/update-check.ts: GitUpdateStatus",
|
||||
"src/infra/update-check.ts: NpmPackageTargetStatus",
|
||||
"src/infra/update-check.ts: NpmTagStatus",
|
||||
"src/infra/update-check.ts: PackageManager",
|
||||
"src/infra/update-check.ts: RegistryStatus",
|
||||
"src/infra/update-global.ts: isExplicitPackageInstallSpec",
|
||||
"src/infra/update-global.ts: isMainPackageTarget",
|
||||
"src/infra/update-global.ts: OPENCLAW_MAIN_PACKAGE_SPEC",
|
||||
"src/infra/update-global.ts: resolveGlobalInstallCommand",
|
||||
"src/infra/update-global.ts: resolveGlobalRoot",
|
||||
"src/infra/update-managed-service-handoff.ts: ManagedServiceUpdateHandoffResult",
|
||||
"src/infra/update-managed-service-handoff.ts: stripSupervisorHintEnv",
|
||||
"src/infra/update-package-manager.ts: PackageManagerCommandRunner",
|
||||
"src/infra/update-post-core-finalize.ts: PostCoreFinalizeOutcome",
|
||||
"src/infra/update-post-core-finalize.ts: PostCoreFinalizeSpawner",
|
||||
"src/infra/update-runner.ts: UpdateInstallSurface",
|
||||
"src/infra/update-runner.ts: UpdateStepCompletion",
|
||||
"src/infra/voicewake-routing.ts: normalizeVoiceWakeTriggerWord",
|
||||
"src/infra/windows-encoding.ts: parseWindowsCodePage",
|
||||
"src/infra/windows-install-roots.ts: DEFAULT_WINDOWS_SYSTEM_ROOT",
|
||||
"src/infra/windows-install-roots.ts: normalizeWindowsInstallRoot",
|
||||
"src/infra/windows-install-roots.ts: privateTestApi",
|
||||
"src/infra/windows-install-roots.ts: resetWindowsInstallRootsForTests",
|
||||
"src/infra/windows-shell-command.ts: RebuiltShellCommandResult",
|
||||
"src/infra/windows-shell-command.ts: ShellSegmentRenderResult",
|
||||
"src/llm/providers/stream-wrappers/anthropic-family-cache-semantics.ts: isAnthropicBedrockModel",
|
||||
"src/llm/utils/node-http-proxy.ts: resolveHttpProxyUrlForTarget",
|
||||
"src/llm/utils/node-http-proxy.ts: UNSUPPORTED_PROXY_PROTOCOL_MESSAGE",
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization";
|
||||
import { writeJson } from "../../src/infra/json-files.ts";
|
||||
import {
|
||||
collectPackageDistInventory,
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
} from "../../src/infra/package-dist-inventory.ts";
|
||||
|
||||
export { LOCAL_BUILD_METADATA_DIST_PATHS } from "./local-build-metadata-paths.mjs";
|
||||
export { PACKAGE_DIST_INVENTORY_RELATIVE_PATH };
|
||||
|
||||
const INSTALL_STAGE_DEBRIS_DIR_PATTERN = /^\.openclaw-install-stage(?:-[^/]+)?$/iu;
|
||||
|
||||
function normalizeRelativePath(value: string): string {
|
||||
return value.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
function isInstallStageDirName(value: string): boolean {
|
||||
return INSTALL_STAGE_DEBRIS_DIR_PATTERN.test(value);
|
||||
}
|
||||
|
||||
export function isLegacyPluginDependencyInstallStagePath(relativePath: string): boolean {
|
||||
const parts = normalizeRelativePath(relativePath).split("/");
|
||||
return (
|
||||
parts.length >= 4 &&
|
||||
parts[0]?.toLowerCase() === "dist" &&
|
||||
parts[1]?.toLowerCase() === "extensions" &&
|
||||
Boolean(parts[2]) &&
|
||||
isInstallStageDirName(parts[3] ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
async function collectLegacyPluginDependencyStagingDebrisPaths(
|
||||
packageRoot: string,
|
||||
): Promise<string[]> {
|
||||
const distDirs: string[] = [];
|
||||
try {
|
||||
const packageRootEntries = await fs.readdir(packageRoot, { withFileTypes: true });
|
||||
for (const entry of packageRootEntries) {
|
||||
if (entry.isDirectory() && entry.name.toLowerCase() === "dist") {
|
||||
distDirs.push(path.join(packageRoot, entry.name));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const debris: string[] = [];
|
||||
for (const distDir of distDirs) {
|
||||
let distEntries: import("node:fs").Dirent[];
|
||||
try {
|
||||
distEntries = await fs.readdir(distDir, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
for (const distEntry of distEntries) {
|
||||
if (!distEntry.isDirectory() || distEntry.name.toLowerCase() !== "extensions") {
|
||||
continue;
|
||||
}
|
||||
const extensionsDir = path.join(distDir, distEntry.name);
|
||||
let extensionEntries: import("node:fs").Dirent[];
|
||||
try {
|
||||
extensionEntries = await fs.readdir(extensionsDir, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
for (const extensionEntry of extensionEntries) {
|
||||
if (!extensionEntry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const extensionPath = path.join(extensionsDir, extensionEntry.name);
|
||||
let stagingEntries: import("node:fs").Dirent[];
|
||||
try {
|
||||
stagingEntries = await fs.readdir(extensionPath, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
for (const stagingEntry of stagingEntries) {
|
||||
if (!isInstallStageDirName(stagingEntry.name)) {
|
||||
continue;
|
||||
}
|
||||
debris.push(
|
||||
normalizeRelativePath(
|
||||
path.relative(packageRoot, path.join(extensionPath, stagingEntry.name)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return debris.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
async function assertNoLegacyPluginDependencyStagingDebris(packageRoot: string): Promise<void> {
|
||||
const debris = await collectLegacyPluginDependencyStagingDebrisPaths(packageRoot);
|
||||
if (debris.length === 0) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`unexpected legacy plugin dependency staging debris in package dist: ${debris.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function writePackageDistInventory(packageRoot: string): Promise<string[]> {
|
||||
await assertNoLegacyPluginDependencyStagingDebris(packageRoot);
|
||||
const inventory = sortUniqueStrings(await collectPackageDistInventory(packageRoot));
|
||||
const inventoryPath = path.join(packageRoot, PACKAGE_DIST_INVENTORY_RELATIVE_PATH);
|
||||
await writeJson(inventoryPath, inventory, { trailingNewline: true });
|
||||
return inventory;
|
||||
}
|
||||
@@ -11,8 +11,8 @@
|
||||
"src/commands/doctor/shared/codex-route-warnings.ts": 3,
|
||||
"src/config/sessions/session-accessor.sqlite.ts": 2,
|
||||
"src/config/sessions/session-accessor.ts": 12,
|
||||
"src/config/sessions/store-load.ts": 5,
|
||||
"src/config/sessions/store.ts": 15,
|
||||
"src/config/sessions/store-load.ts": 3,
|
||||
"src/config/sessions/store.ts": 10,
|
||||
"src/config/sessions/test-helpers.ts": 2,
|
||||
"src/config/sessions/transcript.ts": 2
|
||||
},
|
||||
@@ -31,7 +31,7 @@
|
||||
"sessionLifecycleCleanup": {
|
||||
"src/config/sessions/session-accessor.sqlite.ts": 2,
|
||||
"src/config/sessions/store-maintenance-operations.ts": 2,
|
||||
"src/config/sessions/store.ts": 5
|
||||
"src/config/sessions/store.ts": 2
|
||||
},
|
||||
"transcriptWriter": {
|
||||
"src/agents/embedded-agent-runner/compaction-hooks.ts": 2,
|
||||
@@ -39,7 +39,6 @@
|
||||
"src/agents/embedded-agent-runner/tool-result-truncation.ts": 3,
|
||||
"src/agents/embedded-agent-runner/transcript-rewrite.ts": 3,
|
||||
"src/config/sessions/session-accessor.sqlite.ts": 3,
|
||||
"src/config/sessions/session-accessor.ts": 2,
|
||||
"src/config/sessions/store.ts": 2
|
||||
"src/config/sessions/session-accessor.ts": 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,17 @@ import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
LOCAL_BUILD_METADATA_DIST_PATHS,
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
writePackageDistInventory,
|
||||
} from "../src/infra/package-dist-inventory.ts";
|
||||
import {
|
||||
compareReleaseVersions as compareReleaseVersionsBase,
|
||||
collectReleaseVersionFloorErrors as collectReleaseVersionFloorErrorsBase,
|
||||
resolveNpmDistTagMirrorAuth as resolveNpmDistTagMirrorAuthBase,
|
||||
parseReleaseVersion as parseReleaseVersionBase,
|
||||
} from "./lib/npm-publish-plan.mjs";
|
||||
import {
|
||||
LOCAL_BUILD_METADATA_DIST_PATHS,
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
writePackageDistInventory,
|
||||
} from "./lib/package-dist-inventory.ts";
|
||||
import { WORKSPACE_TEMPLATE_PACK_PATHS } from "./lib/workspace-bootstrap-smoke.mjs";
|
||||
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { spawnSync, type SpawnSyncOptions } from "node:child_process";
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { formatErrorMessage } from "../src/infra/errors.ts";
|
||||
import { writePackageDistInventory } from "../src/infra/package-dist-inventory.ts";
|
||||
import { writePackageDistInventory } from "./lib/package-dist-inventory.ts";
|
||||
import { preparePackageChangelog } from "./package-changelog.mjs";
|
||||
import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs";
|
||||
const FULL_GIT_COMMIT_RE = /^[0-9a-f]{40}$/iu;
|
||||
|
||||
@@ -742,7 +742,7 @@ async function main() {
|
||||
"tsx",
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
"const { writePackageDistInventory } = await import('./src/infra/package-dist-inventory.ts'); await writePackageDistInventory(process.cwd());",
|
||||
"const { writePackageDistInventory } = await import('./scripts/lib/package-dist-inventory.ts'); await writePackageDistInventory(process.cwd());",
|
||||
],
|
||||
sourceDir,
|
||||
{
|
||||
|
||||
@@ -20,12 +20,6 @@ import { basename, dirname, join, resolve, win32 } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { expectDefined } from "../packages/normalization-core/src/expect.js";
|
||||
import { COMPLETION_SKIP_PLUGIN_COMMANDS_ENV } from "../src/cli/completion-runtime.ts";
|
||||
import {
|
||||
isLegacyPluginDependencyInstallStagePath,
|
||||
LOCAL_BUILD_METADATA_DIST_PATHS,
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
writePackageDistInventory,
|
||||
} from "../src/infra/package-dist-inventory.ts";
|
||||
import { escapeRegExp } from "../src/shared/regexp.js";
|
||||
import { checkCliBootstrapExternalImports } from "./check-cli-bootstrap-imports.mjs";
|
||||
import {
|
||||
@@ -38,6 +32,12 @@ import {
|
||||
listBundledPluginPackArtifacts,
|
||||
} from "./lib/bundled-plugin-build-entries.mjs";
|
||||
import { collectPackUnpackedSizeErrors as collectNpmPackUnpackedSizeErrors } from "./lib/npm-pack-budget.mjs";
|
||||
import {
|
||||
isLegacyPluginDependencyInstallStagePath,
|
||||
LOCAL_BUILD_METADATA_DIST_PATHS,
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
writePackageDistInventory,
|
||||
} from "./lib/package-dist-inventory.ts";
|
||||
import { collectBundledPluginPackageDependencySpecs } from "./lib/plugin-package-dependencies.mjs";
|
||||
import {
|
||||
listPluginSdkDistArtifacts,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Write Package Dist Inventory script supports OpenClaw repository automation.
|
||||
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { writePackageDistInventory } from "../src/infra/package-dist-inventory.ts";
|
||||
import { writePackageDistInventory } from "./lib/package-dist-inventory.ts";
|
||||
|
||||
async function writeCurrentPackageDistInventory(): Promise<void> {
|
||||
await writePackageDistInventory(process.cwd());
|
||||
|
||||
@@ -4,7 +4,6 @@ import { afterEach, beforeEach, expect, vi } from "vitest";
|
||||
import { resetAcpManagerTaskStateForTests } from "../../../test/helpers/acp-manager-task-state.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { AcpSessionRuntimeOptions, SessionAcpMeta } from "../../config/sessions/types.js";
|
||||
import { resetHeartbeatWakeStateForTests } from "../../infra/heartbeat-wake.js";
|
||||
import { deleteTestEnvValue, setTestEnvValue } from "../../test-utils/env.js";
|
||||
import { resetAcpActiveTurnsForTests } from "./active-turns.js";
|
||||
|
||||
@@ -338,7 +337,6 @@ export function installAcpSessionManagerTestLifecycle(): void {
|
||||
} else {
|
||||
setTestEnvValue("OPENCLAW_STATE_DIR", ORIGINAL_STATE_DIR);
|
||||
}
|
||||
resetHeartbeatWakeStateForTests();
|
||||
resetAcpManagerTaskStateForTests();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,19 +37,23 @@ let resetProcessRegistryForTests: typeof import("./bash-process-registry.js").re
|
||||
let resolveExecTarget: typeof import("./bash-tools.exec-runtime.js").resolveExecTarget;
|
||||
let runExecProcess: typeof import("./bash-tools.exec-runtime.js").runExecProcess;
|
||||
let prepareGatewaySuspend: typeof import("../infra/gateway-suspend-coordinator.js").prepareGatewaySuspend;
|
||||
let resetGatewaySuspendCoordinatorForTest: typeof import("../infra/gateway-suspend-coordinator.js").resetGatewaySuspendCoordinatorForTest;
|
||||
let resetGatewaySuspendCoordinatorForLifecycleRestart: typeof import("../infra/gateway-suspend-coordinator.js").resetGatewaySuspendCoordinatorForLifecycleRestart;
|
||||
let resumeGatewaySuspend: typeof import("../infra/gateway-suspend-coordinator.js").resumeGatewaySuspend;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ getActiveBackgroundExecSessionCount, markBackgrounded, resetProcessRegistryForTests } =
|
||||
await import("./bash-process-registry.js"));
|
||||
({ resolveExecTarget, runExecProcess } = await import("./bash-tools.exec-runtime.js"));
|
||||
({ prepareGatewaySuspend, resetGatewaySuspendCoordinatorForTest, resumeGatewaySuspend } =
|
||||
({
|
||||
prepareGatewaySuspend,
|
||||
resetGatewaySuspendCoordinatorForLifecycleRestart,
|
||||
resumeGatewaySuspend,
|
||||
} =
|
||||
await import("../infra/gateway-suspend-coordinator.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resetGatewaySuspendCoordinatorForTest();
|
||||
resetGatewaySuspendCoordinatorForLifecycleRestart();
|
||||
resetProcessRegistryForTests();
|
||||
requestHeartbeatMock.mockClear();
|
||||
enqueueSystemEventMock.mockClear();
|
||||
@@ -57,7 +61,6 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetGatewaySuspendCoordinatorForTest();
|
||||
resetProcessRegistryForTests();
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { OPENCLAW_CLI_ENV_VALUE } from "../infra/openclaw-exec-env.js";
|
||||
import type { ExecuteNodeHostCommandParams } from "./bash-tools.exec-host-node.types.js";
|
||||
import type { BashSandboxConfig } from "./bash-tools.shared.js";
|
||||
import type { ExtensionContext } from "./sessions/index.js";
|
||||
@@ -17,6 +16,7 @@ declare module "../plugins/hook-types.js" {
|
||||
}
|
||||
|
||||
const CHANNEL_CONTEXT_ENV_KEY = "OPENCLAW_CHANNEL_CONTEXT";
|
||||
const OPENCLAW_CLI_ENV_VALUE = "1";
|
||||
type CapturedNodeHostParams = Pick<
|
||||
ExecuteNodeHostCommandParams,
|
||||
"env" | "requestedEnv" | "workdir"
|
||||
|
||||
@@ -7,10 +7,7 @@ import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { drainFormattedSystemEvents } from "../auto-reply/reply/session-system-events.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
resetHeartbeatWakeStateForTests,
|
||||
setHeartbeatWakeHandler,
|
||||
} from "../infra/heartbeat-wake.js";
|
||||
import { requestHeartbeat, setHeartbeatWakeHandler } from "../infra/heartbeat-wake.js";
|
||||
import { applyPathPrepend, findPathKey } from "../infra/path-prepend.js";
|
||||
import {
|
||||
peekSystemEventEntries,
|
||||
@@ -820,13 +817,25 @@ describe("exec exit codes", () => {
|
||||
describe("exec notifyOnExit", () => {
|
||||
useCapturedEnv([...SHELL_ENV_KEYS], applyDefaultShellEnv);
|
||||
|
||||
beforeEach(() => {
|
||||
resetHeartbeatWakeStateForTests();
|
||||
});
|
||||
async function drainPendingHeartbeatWakes(): Promise<void> {
|
||||
const handler = vi.fn(async () => ({ status: "ran" as const, durationMs: 0 }));
|
||||
const dispose = setHeartbeatWakeHandler(handler);
|
||||
try {
|
||||
requestHeartbeat({
|
||||
source: "other",
|
||||
intent: "immediate",
|
||||
reason: "test-cleanup",
|
||||
coalesceMs: 0,
|
||||
});
|
||||
await expect.poll(() => handler.mock.calls.length, NOTIFY_POLL_OPTIONS).toBeGreaterThan(0);
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
resetHeartbeatWakeStateForTests();
|
||||
});
|
||||
beforeEach(drainPendingHeartbeatWakes);
|
||||
|
||||
afterEach(drainPendingHeartbeatWakes);
|
||||
|
||||
it("enqueues a system event when a backgrounded exec exits", async () => {
|
||||
const tool = createNotifyOnExitExecTool();
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
claimAgentRunContext,
|
||||
getAgentEventLifecycleGeneration,
|
||||
getAgentRunContext,
|
||||
resetAgentRunContextForTest,
|
||||
resetAgentEventsForTest,
|
||||
rotateAgentEventLifecycleGeneration,
|
||||
withAgentRunLifecycleGeneration,
|
||||
} from "../../infra/agent-events.js";
|
||||
@@ -301,6 +301,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resetAgentEventsForTest();
|
||||
resetRunOverflowCompactionHarnessMocks();
|
||||
mockedBuildEmbeddedRunPayloads.mockReturnValue([{ text: "ok" }]);
|
||||
});
|
||||
@@ -1117,7 +1118,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
|
||||
});
|
||||
|
||||
it("rebinds preserved queued work to the current lifecycle generation", async () => {
|
||||
resetAgentRunContextForTest();
|
||||
mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null }));
|
||||
let enqueueCount = 0;
|
||||
let runQueuedTask: (() => void) | undefined;
|
||||
@@ -1157,7 +1157,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
|
||||
expect(onExecutionStarted).toHaveBeenCalledWith({
|
||||
lifecycleGeneration: currentLifecycleGeneration,
|
||||
});
|
||||
resetAgentRunContextForTest();
|
||||
});
|
||||
|
||||
it("revalidates reserved harness ownership after the global queue wait", async () => {
|
||||
@@ -1222,7 +1221,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
|
||||
});
|
||||
|
||||
it("rejects background work queued across lifecycle rotation", async () => {
|
||||
resetAgentRunContextForTest();
|
||||
let enqueueCount = 0;
|
||||
let runQueuedTask: (() => void) | undefined;
|
||||
const runPromise = runEmbeddedAgent({
|
||||
@@ -1256,7 +1254,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
|
||||
});
|
||||
|
||||
it("does not claim a rebound generation after queued work was aborted", async () => {
|
||||
resetAgentRunContextForTest();
|
||||
let enqueueCount = 0;
|
||||
let runQueuedTask: (() => void) | undefined;
|
||||
const abortController = new AbortController();
|
||||
@@ -1292,7 +1289,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
|
||||
});
|
||||
|
||||
it("rejects stale descendants admitted after lifecycle rotation", async () => {
|
||||
resetAgentRunContextForTest();
|
||||
const preRestartGeneration = getAgentEventLifecycleGeneration();
|
||||
rotateAgentEventLifecycleGeneration();
|
||||
|
||||
@@ -3815,7 +3811,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
|
||||
});
|
||||
|
||||
it("does not let an old execution rotate a newer same-id run context", async () => {
|
||||
resetAgentRunContextForTest();
|
||||
const currentLifecycleGeneration = rotateAgentEventLifecycleGeneration();
|
||||
claimAgentRunContext("shared-run", {
|
||||
sessionKey: "new-session-key",
|
||||
@@ -3845,7 +3840,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
|
||||
lifecycleGeneration: currentLifecycleGeneration,
|
||||
}),
|
||||
);
|
||||
resetAgentRunContextForTest();
|
||||
});
|
||||
|
||||
it("guards thrown engine-owned overflow compaction attempts", async () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { drainSessionStoreWriterQueuesForTest } from "../config/sessions.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
readCompactionCount,
|
||||
seedSessionStore,
|
||||
@@ -130,10 +129,6 @@ function loggedInfoMessageAt(info: ReturnType<typeof vi.fn>, index: number): str
|
||||
return message;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await drainSessionStoreWriterQueuesForTest();
|
||||
});
|
||||
|
||||
describe("reconcileSessionStoreCompactionCountAfterSuccess", () => {
|
||||
it("raises the stored compaction count to the observed value", async () => {
|
||||
// Store count can lag the in-memory count after async writes; reconciliation
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "../../infra/diagnostic-events.js";
|
||||
import {
|
||||
getActiveDiagnosticTraceContext,
|
||||
resetDiagnosticTraceContextForTest,
|
||||
runWithDiagnosticTraceContext,
|
||||
type DiagnosticTraceContext,
|
||||
} from "../../infra/diagnostic-trace-context.js";
|
||||
@@ -134,7 +133,6 @@ function captureDiagnosticEvents(
|
||||
describe("AgentHarness lifecycle runner", () => {
|
||||
afterEach(() => {
|
||||
resetDiagnosticEventsForTest();
|
||||
resetDiagnosticTraceContextForTest();
|
||||
});
|
||||
|
||||
it("runs a harness attempt without changing attempt params", async () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { callGateway } from "../gateway/call.js";
|
||||
import {
|
||||
getAgentEventLifecycleGeneration,
|
||||
registerAgentRunContext,
|
||||
resetAgentRunContextForTest,
|
||||
resetAgentEventsForTest,
|
||||
} from "../infra/agent-events.js";
|
||||
import {
|
||||
getActiveGatewayRootWorkCount,
|
||||
@@ -69,7 +69,7 @@ let tmpDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
resetAgentRunContextForTest();
|
||||
resetAgentEventsForTest();
|
||||
resetGatewayWorkAdmission();
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-main-restart-recovery-"));
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { FetchLike } from "@modelcontextprotocol/sdk/shared/transport.js";
|
||||
* Verifies SSRF-guarded fetch, scoped dispatcher behavior, and same-origin headers.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TEST_UNDICI_RUNTIME_DEPS_KEY } from "../infra/net/undici-runtime.js";
|
||||
import {
|
||||
buildMcpHttpFetch,
|
||||
withoutMcpAuthorizationHeader,
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
} from "./mcp-http-fetch.js";
|
||||
|
||||
const testGlobal = globalThis as Record<string, unknown>;
|
||||
const TEST_UNDICI_RUNTIME_DEPS_KEY = "__OPENCLAW_TEST_UNDICI_RUNTIME_DEPS__";
|
||||
const { lookupMock } = vi.hoisted(() => ({
|
||||
lookupMock: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -3,10 +3,12 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { normalizeConfigPatchReplacePath } from "../config/patch-replace-paths.js";
|
||||
import { GatewayClientRequestError } from "../gateway/client.js";
|
||||
import { readRestartSentinel } from "../infra/restart-sentinel.js";
|
||||
import { testing as restartTesting } from "../infra/restart.js";
|
||||
import {
|
||||
resetGatewayRestartStateForInProcessRestart,
|
||||
setPreRestartDeferralCheck,
|
||||
} from "../infra/restart.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import { createGatewayTool } from "./tools/gateway-tool.js";
|
||||
import { callGatewayTool } from "./tools/gateway.js";
|
||||
@@ -131,7 +133,6 @@ function expectConfigMutationCall(params: {
|
||||
|
||||
describe("gateway tool", () => {
|
||||
beforeEach(() => {
|
||||
restartTesting.resetSigusr1State();
|
||||
callGatewayToolMock.mockClear();
|
||||
readGatewayCallOptionsMock.mockClear();
|
||||
callGatewayToolMock.mockImplementation(async (method: string) => {
|
||||
@@ -298,12 +299,10 @@ describe("gateway tool", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("schedules SIGUSR1 restart", async () => {
|
||||
it("schedules SIGUSR1 restart and writes the routed sentinel", async () => {
|
||||
resetGatewayRestartStateForInProcessRestart();
|
||||
setPreRestartDeferralCheck(() => 0);
|
||||
const kill = vi.spyOn(process, "kill").mockImplementation(() => true);
|
||||
const restartSignalKillCalls = () =>
|
||||
kill.mock.calls.filter(
|
||||
([pid, signal]) => pid === process.pid && (signal === "SIGUSR1" || signal === undefined),
|
||||
);
|
||||
const sigusr1Handler = vi.fn();
|
||||
process.on("SIGUSR1", sigusr1Handler);
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-"));
|
||||
@@ -312,9 +311,7 @@ describe("gateway tool", () => {
|
||||
await withEnvAsync(
|
||||
{ OPENCLAW_STATE_DIR: stateDir, OPENCLAW_PROFILE: "isolated" },
|
||||
async () => {
|
||||
const tool = requireGatewayTool();
|
||||
|
||||
const result = await tool.execute("call1", {
|
||||
const result = await requireGatewayTool().execute("call1", {
|
||||
action: "restart",
|
||||
delayMs: 0,
|
||||
});
|
||||
@@ -325,13 +322,11 @@ describe("gateway tool", () => {
|
||||
delayMs: 0,
|
||||
});
|
||||
|
||||
expect(restartSignalKillCalls()).toHaveLength(0);
|
||||
expect(sigusr1Handler).not.toHaveBeenCalled();
|
||||
await vi.waitFor(() => expect(sigusr1Handler).toHaveBeenCalledTimes(1), {
|
||||
interval: 1,
|
||||
timeout: 1_000,
|
||||
});
|
||||
expect(restartSignalKillCalls()).toHaveLength(0);
|
||||
expect(kill).not.toHaveBeenCalled();
|
||||
|
||||
const sentinel = await readRestartSentinel();
|
||||
expect(sentinel?.payload.kind).toBe("restart");
|
||||
@@ -343,7 +338,8 @@ describe("gateway tool", () => {
|
||||
} finally {
|
||||
process.removeListener("SIGUSR1", sigusr1Handler);
|
||||
kill.mockRestore();
|
||||
restartTesting.resetSigusr1State();
|
||||
resetGatewayRestartStateForInProcessRestart();
|
||||
setPreRestartDeferralCheck(() => 0);
|
||||
await fs.rm(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -501,18 +497,6 @@ describe("gateway tool", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("distinguishes explicit terminal array consent from indexed consent", () => {
|
||||
expect(normalizeConfigPatchReplacePath("bindings[]")).toBe("bindings");
|
||||
expect(normalizeConfigPatchReplacePath("bindings[0]")).toBe("bindings[0]");
|
||||
expect(normalizeConfigPatchReplacePath("agents.list[0].skills")).toBe("agents.list[].skills");
|
||||
expect(normalizeConfigPatchReplacePath(normalizeConfigPatchReplacePath("bindings[]"))).toBe(
|
||||
"bindings",
|
||||
);
|
||||
expect(normalizeConfigPatchReplacePath(normalizeConfigPatchReplacePath("bindings[0]"))).toBe(
|
||||
"bindings[0]",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects config.patch when it changes safe bin approval paths", async () => {
|
||||
const tool = requireGatewayTool();
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Verifies Docker create arguments for sandbox hardening and configured passthrough.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { OPENCLAW_CLI_ENV_VALUE } from "../infra/openclaw-exec-env.js";
|
||||
import { SANDBOX_DOCKER_CREATE_ARGS_EPOCH } from "./sandbox/constants.js";
|
||||
import { buildSandboxCreateArgs } from "./sandbox/docker.js";
|
||||
import type { SandboxDockerConfig } from "./sandbox/types.js";
|
||||
|
||||
const OPENCLAW_CLI_ENV_VALUE = "1";
|
||||
|
||||
describe("buildSandboxCreateArgs", () => {
|
||||
function createSandboxConfig(
|
||||
overrides: Partial<SandboxDockerConfig> = {},
|
||||
|
||||
@@ -11,15 +11,12 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { setRuntimeConfigSnapshot } from "../config/config.js";
|
||||
import {
|
||||
clearSessionStoreCacheForTest,
|
||||
drainSessionStoreWriterQueuesForTest,
|
||||
} from "../config/sessions/store.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { createRunningTaskRun } from "../tasks/detached-task-runtime.js";
|
||||
import { resetTaskFlowRegistryForTests } from "../tasks/task-flow-registry.js";
|
||||
import { findTaskByRunId, resetTaskRegistryForTests } from "../tasks/task-registry.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import { cleanupSessionStateForTest } from "../test-utils/session-state-cleanup.js";
|
||||
import { recoverOrphanedSubagentSessions } from "./subagent-orphan-recovery.js";
|
||||
import {
|
||||
addSubagentRunForTests,
|
||||
@@ -84,8 +81,7 @@ describe("subagent orphan recovery — faithful restart path", () => {
|
||||
afterEach(async () => {
|
||||
testing.setDepsForTest();
|
||||
resetSubagentRegistryForTests({ persist: false });
|
||||
await drainSessionStoreWriterQueuesForTest();
|
||||
clearSessionStoreCacheForTest();
|
||||
await cleanupSessionStateForTest();
|
||||
resetTaskRegistryForTests({ persist: false });
|
||||
resetTaskFlowRegistryForTests({ persist: false });
|
||||
if (tempStateDir) {
|
||||
@@ -95,7 +91,7 @@ describe("subagent orphan recovery — faithful restart path", () => {
|
||||
envSnapshot.restore();
|
||||
});
|
||||
|
||||
it("finalizes a stale (>2h) aborted run in the real registry instead of resuming it", async () => {
|
||||
it("finalizes a stale (>2h) aborted run instead of resuming it", async () => {
|
||||
const now = Date.now();
|
||||
const childSessionKey = "agent:main:subagent:stale-aborted";
|
||||
const runId = "run-stale-aborted";
|
||||
@@ -130,25 +126,11 @@ describe("subagent orphan recovery — faithful restart path", () => {
|
||||
).not.toBeNull();
|
||||
addSubagentRunForTests(record);
|
||||
|
||||
const before = getSubagentRunByChildSessionKey(childSessionKey);
|
||||
console.log(
|
||||
`[proof] before recovery: stale run endedAt=${before?.endedAt ?? "undefined"} outcome=${
|
||||
before?.outcome?.status ?? "undefined"
|
||||
}`,
|
||||
);
|
||||
|
||||
const result = await recoverOrphanedSubagentSessions({
|
||||
getActiveRuns: () => new Map([[runId, record]]),
|
||||
});
|
||||
|
||||
const after = getSubagentRunByChildSessionKey(childSessionKey);
|
||||
console.log(
|
||||
`[proof] after recovery: result=${JSON.stringify(result)} endedAt=${
|
||||
after?.endedAt ?? "undefined"
|
||||
} outcome=${after?.outcome?.status ?? "undefined"}`,
|
||||
);
|
||||
|
||||
// Stale aborted run was finalized in the real registry, not resumed.
|
||||
expect(vi.mocked(callGateway)).not.toHaveBeenCalled();
|
||||
expect(after?.endedAt).toBeTypeOf("number");
|
||||
expect(after?.outcome?.status).toBe("error");
|
||||
@@ -159,11 +141,9 @@ describe("subagent orphan recovery — faithful restart path", () => {
|
||||
error: expect.stringContaining("stale aborted subagent run not resumed"),
|
||||
});
|
||||
|
||||
// The task finalizer and session projection are durable, not only
|
||||
// in-memory side effects of the recovery pass.
|
||||
resetTaskRegistryForTests({ persist: false });
|
||||
expect(findTaskByRunId(runId)).toMatchObject({ status: "failed" });
|
||||
await drainSessionStoreWriterQueuesForTest();
|
||||
await cleanupSessionStateForTest();
|
||||
const persistedSession = (await readSubagentSessionStore(storePath))[childSessionKey];
|
||||
expect(persistedSession).toMatchObject({
|
||||
status: "failed",
|
||||
|
||||
@@ -5,11 +5,8 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import "./subagent-registry.mocks.shared.js";
|
||||
import {
|
||||
clearSessionStoreCacheForTest,
|
||||
drainSessionStoreWriterQueuesForTest,
|
||||
} from "../config/sessions/store.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import { cleanupSessionStateForTest } from "../test-utils/session-state-cleanup.js";
|
||||
import {
|
||||
createSubagentRegistryTestDeps,
|
||||
writeSubagentSessionEntry,
|
||||
@@ -128,8 +125,7 @@ describe("subagent registry persistence resume", () => {
|
||||
announceSpy.mockClear();
|
||||
mod.testing.setDepsForTest();
|
||||
mod.resetSubagentRegistryForTests({ persist: false });
|
||||
await drainSessionStoreWriterQueuesForTest();
|
||||
clearSessionStoreCacheForTest();
|
||||
await cleanupSessionStateForTest();
|
||||
if (tempStateDir) {
|
||||
await fs.rm(tempStateDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
||||
tempStateDir = null;
|
||||
|
||||
@@ -8,14 +8,11 @@ import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import "./subagent-registry.mocks.shared.js";
|
||||
import { replaceSessionEntry } from "../config/sessions/session-accessor.js";
|
||||
import {
|
||||
clearSessionStoreCacheForTest,
|
||||
drainSessionStoreWriterQueuesForTest,
|
||||
} from "../config/sessions/store.js";
|
||||
import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { onAgentEvent } from "../infra/agent-events.js";
|
||||
import { captureEnv, deleteTestEnvValue, setTestEnvValue, withEnv } from "../test-utils/env.js";
|
||||
import { cleanupSessionStateForTest } from "../test-utils/session-state-cleanup.js";
|
||||
import { scheduleOrphanRecovery } from "./subagent-orphan-recovery.js";
|
||||
import { persistSubagentSessionTiming } from "./subagent-registry-helpers.js";
|
||||
import { getSubagentRunsSnapshotForRead } from "./subagent-registry-state.js";
|
||||
@@ -230,8 +227,7 @@ describe("subagent registry persistence", () => {
|
||||
afterEach(async () => {
|
||||
testing.setDepsForTest();
|
||||
resetSubagentRegistryForTests({ persist: false });
|
||||
await drainSessionStoreWriterQueuesForTest();
|
||||
clearSessionStoreCacheForTest();
|
||||
await cleanupSessionStateForTest();
|
||||
if (tempStateDir) {
|
||||
await fs.rm(tempStateDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
||||
tempStateDir = null;
|
||||
|
||||
@@ -5,10 +5,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
resetActiveNodeContextForTests,
|
||||
setActiveNodeContext,
|
||||
} from "../infra/active-node-context.js";
|
||||
import { setActiveNodeContext } from "../infra/active-node-context.js";
|
||||
import { buildSystemPromptParams } from "./system-prompt-params.js";
|
||||
|
||||
async function makeTempDir(label: string): Promise<string> {
|
||||
@@ -35,7 +32,7 @@ function buildParams(params: { config?: OpenClawConfig; workspaceDir?: string; c
|
||||
}
|
||||
|
||||
describe("buildSystemPromptParams", () => {
|
||||
afterEach(() => resetActiveNodeContextForTests());
|
||||
afterEach(() => setActiveNodeContext(null));
|
||||
|
||||
it("projects only the stable active-node identity", () => {
|
||||
setActiveNodeContext({ nodeId: "mac-123" });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Video generation background tests cover detached task lifecycle, keepalive
|
||||
// progress, completion announcement, and direct failure delivery.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getAgentRunContext, resetAgentRunContextForTest } from "../../infra/agent-events.js";
|
||||
import { getAgentRunContext, resetAgentEventsForTest } from "../../infra/agent-events.js";
|
||||
import { VIDEO_GENERATION_TASK_KIND } from "../video-generation-task-status.js";
|
||||
import {
|
||||
announceDeliveryMocks,
|
||||
@@ -28,7 +28,7 @@ const { withMediaGenerationTaskKeepalive } = await import("./media-generate-back
|
||||
|
||||
describe("video generate background helpers", () => {
|
||||
beforeEach(() => {
|
||||
resetAgentRunContextForTest();
|
||||
resetAgentEventsForTest();
|
||||
resetMediaBackgroundMocks({
|
||||
taskExecutorMocks,
|
||||
taskDeliveryRuntimeMocks,
|
||||
@@ -38,7 +38,7 @@ describe("video generate background helpers", () => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
resetAgentRunContextForTest();
|
||||
resetAgentEventsForTest();
|
||||
});
|
||||
|
||||
it("creates a running task with queued progress text", () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Tests command gating rules for ownership, channel, and active session state.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { isCommandFlagEnabled } from "../../config/commands.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { REDACTED_SENTINEL } from "../../config/redact-snapshot.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
@@ -621,18 +620,6 @@ describe("command gating", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores inherited command flags", () => {
|
||||
const inheritedCommands = Object.create({
|
||||
bash: true,
|
||||
config: true,
|
||||
debug: true,
|
||||
}) as Record<string, unknown>;
|
||||
const cfg = { commands: inheritedCommands as never } as OpenClawConfig;
|
||||
expect(isCommandFlagEnabled(cfg, "bash")).toBe(false);
|
||||
expect(isCommandFlagEnabled(cfg, "config")).toBe(false);
|
||||
expect(isCommandFlagEnabled(cfg, "debug")).toBe(false);
|
||||
});
|
||||
|
||||
it("blocks disallowed /config set writes", async () => {
|
||||
resolveConfigWriteDeniedTextMock
|
||||
.mockReturnValueOnce("Config writes are disabled")
|
||||
|
||||
@@ -3,7 +3,6 @@ import { vi, type Mock } from "vitest";
|
||||
import { clearAgentHarnesses } from "../../agents/harness/registry.js";
|
||||
import type { ChannelMessagingAdapter } from "../../channels/plugins/types.core.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { clearApprovalNativeRouteStateForTest } from "../../infra/approval-native-route-coordinator.js";
|
||||
import type {
|
||||
AcpRuntime,
|
||||
AcpRuntimeEnsureInput,
|
||||
@@ -459,7 +458,6 @@ export const describe0BeforeEach0 = () => {
|
||||
...passiveThreadingTestPlugins,
|
||||
]),
|
||||
);
|
||||
clearApprovalNativeRouteStateForTest();
|
||||
acpManagerRuntimeMocks.getAcpSessionManager.mockReset();
|
||||
acpManagerRuntimeMocks.getAcpSessionManager.mockReturnValue(createMockAcpSessionManager());
|
||||
replyRunTesting.resetReplyRunRegistry();
|
||||
|
||||
@@ -9,6 +9,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
|
||||
import { setCliSessionBinding } from "../../agents/cli-session.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import { resetAgentEventsForTest } from "../../infra/agent-events.js";
|
||||
import {
|
||||
createUserTurnTranscriptRecorder,
|
||||
type PersistedUserTurnMessage,
|
||||
@@ -556,6 +557,7 @@ beforeAll(async () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resetAgentEventsForTest({ preserveListeners: true });
|
||||
setFastFollowupCliBackendDeps();
|
||||
replyRunTestingForTest?.resetReplyRunRegistry();
|
||||
clearRuntimeConfigSnapshot?.();
|
||||
@@ -616,6 +618,7 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetAgentEventsForTest({ preserveListeners: true });
|
||||
cliBackendsTestingForTest?.resetDepsForTest();
|
||||
replyRunTestingForTest?.resetReplyRunRegistry();
|
||||
clearRuntimeConfigSnapshot?.();
|
||||
@@ -1115,7 +1118,6 @@ describe("createFollowupRunner reply-lane admission", () => {
|
||||
const realAgentEvents = await vi.importActual<typeof import("../../infra/agent-events.js")>(
|
||||
"../../infra/agent-events.js",
|
||||
);
|
||||
realAgentEvents.resetAgentRunContextForTest();
|
||||
const active = createReplyOperationForTest({
|
||||
sessionKey: "main",
|
||||
sessionId: "pre-compact-session",
|
||||
@@ -1170,7 +1172,6 @@ describe("createFollowupRunner reply-lane admission", () => {
|
||||
expect(realAgentEvents.getAgentRunContext(observedRunId ?? "")?.sessionId).toBe(
|
||||
"post-compact-session",
|
||||
);
|
||||
realAgentEvents.resetAgentRunContextForTest();
|
||||
});
|
||||
|
||||
it("routes preflight compaction failures before starting queued followup runs", async () => {
|
||||
@@ -4695,7 +4696,6 @@ describe("createFollowupRunner compaction", () => {
|
||||
const realAgentEvents = await vi.importActual<typeof import("../../infra/agent-events.js")>(
|
||||
"../../infra/agent-events.js",
|
||||
);
|
||||
realAgentEvents.resetAgentRunContextForTest();
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "old-session",
|
||||
updatedAt: Date.now(),
|
||||
@@ -4751,14 +4751,12 @@ describe("createFollowupRunner compaction", () => {
|
||||
|
||||
expect(observedRunId).toBeDefined();
|
||||
expect(realAgentEvents.getAgentRunContext(observedRunId ?? "")?.sessionId).toBe("new-session");
|
||||
realAgentEvents.resetAgentRunContextForTest();
|
||||
});
|
||||
|
||||
it("captures follow-up lifecycle ownership before asynchronous preflight", async () => {
|
||||
const realAgentEvents = await vi.importActual<typeof import("../../infra/agent-events.js")>(
|
||||
"../../infra/agent-events.js",
|
||||
);
|
||||
realAgentEvents.resetAgentRunContextForTest();
|
||||
const initialGeneration = realAgentEvents.getAgentEventLifecycleGeneration();
|
||||
let releasePreflight: (() => void) | undefined;
|
||||
runPreflightCompactionIfNeededMock.mockImplementationOnce(
|
||||
@@ -4826,7 +4824,6 @@ describe("createFollowupRunner compaction", () => {
|
||||
expect(observedLifecycleGeneration).toBe(initialGeneration);
|
||||
expect(realAgentEvents.getAgentRunContext(registeredRun?.runId ?? "")).toBeUndefined();
|
||||
} finally {
|
||||
realAgentEvents.resetAgentRunContextForTest();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
createChildDiagnosticTraceContext,
|
||||
freezeDiagnosticTraceContext,
|
||||
getActiveDiagnosticTraceContext,
|
||||
resetDiagnosticTraceContextForTest,
|
||||
} from "../../infra/diagnostic-trace-context.js";
|
||||
import { logMessageProcessed } from "../../logging/diagnostic.js";
|
||||
import { getChildLogger, resetLogger, setLoggerOverride } from "../../logging/logger.js";
|
||||
@@ -192,7 +191,6 @@ describe("channel turn kernel", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
resetDiagnosticEventsForTest();
|
||||
resetDiagnosticTraceContextForTest();
|
||||
resetLogger();
|
||||
setLoggerOverride({ level: "info" });
|
||||
resolveOutboundDurableFinalDeliverySupport.mockResolvedValue({ ok: true });
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Daemon restart health tests cover health checks after daemon restart operations.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { GatewayService } from "../../daemon/service.js";
|
||||
import type { PortListenerKind, PortUsage } from "../../infra/ports.js";
|
||||
import type { PortUsage } from "../../infra/ports.js";
|
||||
|
||||
type PortListenerKind = ReturnType<typeof import("../../infra/ports.js").classifyPortListener>;
|
||||
|
||||
const inspectPortUsage = vi.hoisted(() => vi.fn<(port: number) => Promise<PortUsage>>());
|
||||
const sleep = vi.hoisted(() => vi.fn(async (_ms: number) => {}));
|
||||
|
||||
@@ -5,13 +5,17 @@ import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { StaleOpenClawUpdateLaunchdJob } from "../../daemon/launchd.js";
|
||||
import { createMockGatewayService } from "../../daemon/service.test-helpers.js";
|
||||
import type { PortConnections, PortListener, PortUsageStatus } from "../../infra/ports.js";
|
||||
import type { PortListener, PortUsageStatus } from "../../infra/ports.js";
|
||||
import type { GatewayRestartHandoff } from "../../infra/restart-handoff.js";
|
||||
import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../../test-utils/env.js";
|
||||
import { VERSION } from "../../version.js";
|
||||
import type { GatewayRestartSnapshot } from "./restart-health.js";
|
||||
import { gatherDaemonStatus } from "./status.gather.js";
|
||||
|
||||
type PortConnections = Awaited<
|
||||
ReturnType<typeof import("../../infra/ports.js").inspectPortConnections>
|
||||
>;
|
||||
|
||||
const callGatewayStatusProbe = vi.fn<
|
||||
(opts?: unknown) => Promise<{
|
||||
ok: boolean;
|
||||
|
||||
@@ -1129,6 +1129,7 @@ describe("runGatewayLoop", () => {
|
||||
expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(1);
|
||||
expect(markGatewayDraining).toHaveBeenCalledTimes(1);
|
||||
expect(resetAllLanes).toHaveBeenCalledTimes(1);
|
||||
expect(resetGatewaySuspendCoordinatorForLifecycleRestart).toHaveBeenCalledTimes(1);
|
||||
expect(resetGatewayRestartStateForInProcessRestart).toHaveBeenCalledTimes(1);
|
||||
expect(reloadTaskRuntimeStateFromStore).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
@@ -1318,6 +1319,7 @@ describe("runGatewayLoop", () => {
|
||||
expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(2);
|
||||
expect(markGatewayDraining).toHaveBeenCalledTimes(2);
|
||||
expect(resetAllLanes).toHaveBeenCalledTimes(2);
|
||||
expect(resetGatewaySuspendCoordinatorForLifecycleRestart).toHaveBeenCalledTimes(2);
|
||||
expect(resetGatewayRestartStateForInProcessRestart).toHaveBeenCalledTimes(2);
|
||||
expect(reloadTaskRuntimeStateFromStore).toHaveBeenCalledTimes(2);
|
||||
expect(acquireGatewayLock).toHaveBeenCalledTimes(3);
|
||||
@@ -1390,6 +1392,7 @@ describe("runGatewayLoop", () => {
|
||||
expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(2);
|
||||
expect(markGatewayDraining).toHaveBeenCalledTimes(2);
|
||||
expect(resetAllLanes).toHaveBeenCalledTimes(2);
|
||||
expect(resetGatewaySuspendCoordinatorForLifecycleRestart).toHaveBeenCalledTimes(2);
|
||||
expect(resetGatewayRestartStateForInProcessRestart).toHaveBeenCalledTimes(2);
|
||||
expect(reloadTaskRuntimeStateFromStore).toHaveBeenCalledTimes(2);
|
||||
expect(acquireGatewayLock).toHaveBeenCalledTimes(3);
|
||||
|
||||
@@ -7,12 +7,12 @@ import path from "node:path";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { Command } from "commander";
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { writePackageDistInventory } from "../../scripts/lib/package-dist-inventory.ts";
|
||||
import { TEST_BUNDLED_RUNTIME_SIDECAR_PATHS } from "../../test/helpers/bundled-runtime-sidecars.js";
|
||||
import type { OpenClawConfig, ConfigFileSnapshot } from "../config/types.openclaw.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { GATEWAY_SERVICE_RUNTIME_PID_ENV } from "../daemon/constants.js";
|
||||
import type { ClawHubRiskAcknowledgementRequest } from "../infra/clawhub-install-trust.js";
|
||||
import { writePackageDistInventory } from "../infra/package-dist-inventory.js";
|
||||
import { isBetaTag } from "../infra/update-channels.js";
|
||||
import {
|
||||
createDeferredConfiguredPluginRepairDoctorResult,
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
emitAgentEvent,
|
||||
onAgentEvent,
|
||||
resetAgentEventsForTest,
|
||||
resetAgentRunContextForTest,
|
||||
} from "../infra/agent-events.js";
|
||||
import type { PluginProviderRegistration } from "../plugins/registry.test-fixtures.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
@@ -393,7 +392,6 @@ beforeEach(() => {
|
||||
installThinkingTestProviders();
|
||||
clearSessionStoreCacheForTest();
|
||||
resetAgentEventsForTest();
|
||||
resetAgentRunContextForTest();
|
||||
acpManagerTesting.resetAcpSessionManagerForTests();
|
||||
runtimeSnapshotModule.clearRuntimeConfigSnapshot();
|
||||
vi.mocked(runEmbeddedAgent).mockResolvedValue(createDefaultAgentResult());
|
||||
|
||||
@@ -2,16 +2,12 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { __testing as openClawRootTesting } from "../infra/openclaw-root.js";
|
||||
import { resolveSandboxScript } from "./doctor-sandbox.js";
|
||||
|
||||
describe("resolveSandboxScript", () => {
|
||||
const created: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
// The shared resolver memoizes package-root lookups process-wide; reset so per-test temp dirs
|
||||
// never see a stale (including negative) cache hit.
|
||||
openClawRootTesting.clearOpenClawPackageRootCaches();
|
||||
for (const dir of created.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "../config/sessions/store.js";
|
||||
import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { __testing as openClawRootTesting } from "../infra/openclaw-root.js";
|
||||
import { AGENT_HARNESS_SESSION_KEY_RESERVED_MESSAGE } from "../sessions/agent-harness-session-key.js";
|
||||
import type { Skill } from "../skills/loading/skill-contract.js";
|
||||
|
||||
@@ -126,7 +125,6 @@ describe("doctor session snapshot stale runtime metadata", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
openClawRootTesting.clearOpenClawPackageRootCaches();
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -20,10 +20,7 @@ interface BackupMaintenanceFs extends BackupRotationFs {
|
||||
* Missing slots are ignored so interrupted writes or first-run configs do not
|
||||
* block the next config write.
|
||||
*/
|
||||
export async function rotateConfigBackups(
|
||||
configPath: string,
|
||||
ioFs: BackupRotationFs,
|
||||
): Promise<void> {
|
||||
async function rotateConfigBackups(configPath: string, ioFs: BackupRotationFs): Promise<void> {
|
||||
if (CONFIG_BACKUP_COUNT <= 1) {
|
||||
return;
|
||||
}
|
||||
@@ -48,10 +45,7 @@ export async function rotateConfigBackups(
|
||||
* Backups are copied on mixed filesystems, so copy mode preservation is not a
|
||||
* portable security guarantee.
|
||||
*/
|
||||
export async function hardenBackupPermissions(
|
||||
configPath: string,
|
||||
ioFs: BackupRotationFs,
|
||||
): Promise<void> {
|
||||
async function hardenBackupPermissions(configPath: string, ioFs: BackupRotationFs): Promise<void> {
|
||||
if (!ioFs.chmod) {
|
||||
return;
|
||||
}
|
||||
@@ -67,10 +61,7 @@ export async function hardenBackupPermissions(
|
||||
}
|
||||
|
||||
/** Prunes stale `.bak.*` files that are outside the managed numbered ring. */
|
||||
export async function cleanOrphanBackups(
|
||||
configPath: string,
|
||||
ioFs: BackupRotationFs,
|
||||
): Promise<void> {
|
||||
async function cleanOrphanBackups(configPath: string, ioFs: BackupRotationFs): Promise<void> {
|
||||
if (!ioFs.readdir) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import type { PluginOrigin } from "../plugins/plugin-origin.types.js";
|
||||
import type { ChannelUiMetadata, PluginUiMetadata } from "./schema.js";
|
||||
|
||||
export type ChannelSchemaMetadataWithOwnership = ChannelUiMetadata & {
|
||||
type ChannelSchemaMetadataWithOwnership = ChannelUiMetadata & {
|
||||
schemaPluginId?: string;
|
||||
schemaPluginOrigin?: PluginOrigin;
|
||||
};
|
||||
@@ -17,7 +17,7 @@ type ChannelMetadataRecord = ChannelSchemaMetadataWithOwnership & {
|
||||
|
||||
type ChannelDmAllowFromMode = "topOnly" | "topOrNested" | "nestedOnly";
|
||||
|
||||
export type ChannelDmPolicyMetadata = {
|
||||
type ChannelDmPolicyMetadata = {
|
||||
id: string;
|
||||
dmAllowFromMode?: ChannelDmAllowFromMode;
|
||||
};
|
||||
|
||||
@@ -3,9 +3,8 @@ import path from "node:path";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { isCommandFlagEnabled, isRestartEnabled } from "./commands.flags.js";
|
||||
import {
|
||||
isCommandFlagEnabled,
|
||||
isRestartEnabled,
|
||||
isNativeCommandsExplicitlyDisabled,
|
||||
resolveNativeCommandsEnabled,
|
||||
resolveNativeSkillsEnabled,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-
|
||||
import type { ChannelId } from "../channels/plugins/types.public.js";
|
||||
import type { NativeCommandsSetting } from "./types.js";
|
||||
import type { OpenClawConfig } from "./types.openclaw.js";
|
||||
export { isCommandFlagEnabled, isRestartEnabled } from "./commands.flags.js";
|
||||
|
||||
function resolveAutoDefault(
|
||||
providerId: ChannelId | undefined,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DiscordConfigSchema,
|
||||
IMessageConfigSchema,
|
||||
IrcConfigSchema,
|
||||
SignalConfigSchema,
|
||||
SlackConfigSchema,
|
||||
TelegramConfigSchema,
|
||||
@@ -95,10 +94,10 @@ describe('account dmPolicy="allowlist" uses inherited allowFrom', () => {
|
||||
schema: SlackConfigSchema,
|
||||
config: {
|
||||
allowFrom: ["U123"],
|
||||
botToken: "xoxb-top",
|
||||
appToken: "xapp-top",
|
||||
botToken: "fake",
|
||||
appToken: "fake",
|
||||
accounts: {
|
||||
work: { dmPolicy: "allowlist", botToken: "xoxb-work", appToken: "xapp-work" },
|
||||
work: { dmPolicy: "allowlist", botToken: "fake", appToken: "fake" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -112,11 +111,6 @@ describe('account dmPolicy="allowlist" uses inherited allowFrom', () => {
|
||||
schema: IMessageConfigSchema,
|
||||
config: { allowFrom: ["alice"], accounts: { work: { dmPolicy: "allowlist" } } },
|
||||
},
|
||||
{
|
||||
name: "irc",
|
||||
schema: IrcConfigSchema,
|
||||
config: { allowFrom: ["nick"], accounts: { work: { dmPolicy: "allowlist" } } },
|
||||
},
|
||||
] as const)(
|
||||
"accepts $name account allowlist when parent allowFrom exists",
|
||||
({ schema, config }) => {
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
// Covers config backup rotation limits and cleanup behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createPreUpdateConfigSnapshot,
|
||||
maintainConfigBackups,
|
||||
rotateConfigBackups,
|
||||
hardenBackupPermissions,
|
||||
cleanOrphanBackups,
|
||||
} from "./backup-rotation.js";
|
||||
import { createPreUpdateConfigSnapshot, maintainConfigBackups } from "./backup-rotation.js";
|
||||
import {
|
||||
expectPosixMode,
|
||||
IS_WINDOWS,
|
||||
resolveConfigPathFromTempState,
|
||||
} from "./config.backup-rotation.test-helpers.js";
|
||||
import { withTempHome } from "./test-helpers.js";
|
||||
import type { OpenClawConfig } from "./types.js";
|
||||
|
||||
async function expectRegularFile(filePath: string): Promise<void> {
|
||||
expect((await fs.stat(filePath)).isFile()).toBe(true);
|
||||
@@ -31,94 +24,35 @@ async function expectPathMissing(filePath: string): Promise<void> {
|
||||
}
|
||||
|
||||
describe("config backup rotation", () => {
|
||||
it("keeps a 5-deep backup ring for config writes", async () => {
|
||||
it("keeps five recovery points while preserving the pre-update snapshot", async () => {
|
||||
await withTempHome(async () => {
|
||||
const configPath = resolveConfigPathFromTempState();
|
||||
const buildConfig = (version: number): OpenClawConfig =>
|
||||
({
|
||||
agents: { list: [{ id: `v${version}` }] },
|
||||
}) as OpenClawConfig;
|
||||
|
||||
const writeVersion = async (version: number) => {
|
||||
const json = JSON.stringify(buildConfig(version), null, 2).trimEnd().concat("\n");
|
||||
await fs.writeFile(configPath, json, "utf-8");
|
||||
const writeVersion = (version: number) =>
|
||||
fs.writeFile(configPath, JSON.stringify({ version }), "utf-8");
|
||||
const readVersion = async (suffix = "") => {
|
||||
const raw = await fs.readFile(`${configPath}${suffix}`, "utf-8");
|
||||
return (JSON.parse(raw) as { version: number }).version;
|
||||
};
|
||||
const { existsSync } = await import("node:fs");
|
||||
|
||||
await writeVersion(0);
|
||||
await createPreUpdateConfigSnapshot({
|
||||
configPath,
|
||||
fs: { writeFile: fs.writeFile, readFile: fs.readFile, existsSync },
|
||||
});
|
||||
for (let version = 1; version <= 6; version += 1) {
|
||||
await rotateConfigBackups(configPath, fs);
|
||||
await fs.copyFile(configPath, `${configPath}.bak`).catch(() => {
|
||||
// best-effort
|
||||
});
|
||||
await maintainConfigBackups(configPath, fs);
|
||||
await writeVersion(version);
|
||||
}
|
||||
|
||||
const readName = async (suffix = "") => {
|
||||
const raw = await fs.readFile(`${configPath}${suffix}`, "utf-8");
|
||||
return (
|
||||
(JSON.parse(raw) as { agents?: { list?: Array<{ id?: string }> } }).agents?.list?.[0]
|
||||
?.id ?? null
|
||||
);
|
||||
};
|
||||
|
||||
await expect(readName()).resolves.toBe("v6");
|
||||
await expect(readName(".bak")).resolves.toBe("v5");
|
||||
await expect(readName(".bak.1")).resolves.toBe("v4");
|
||||
await expect(readName(".bak.2")).resolves.toBe("v3");
|
||||
await expect(readName(".bak.3")).resolves.toBe("v2");
|
||||
await expect(readName(".bak.4")).resolves.toBe("v1");
|
||||
await expect(readVersion()).resolves.toBe(6);
|
||||
await expect(readVersion(".bak")).resolves.toBe(5);
|
||||
await expect(readVersion(".bak.1")).resolves.toBe(4);
|
||||
await expect(readVersion(".bak.2")).resolves.toBe(3);
|
||||
await expect(readVersion(".bak.3")).resolves.toBe(2);
|
||||
await expect(readVersion(".bak.4")).resolves.toBe(1);
|
||||
await expectPathMissing(`${configPath}.bak.5`);
|
||||
});
|
||||
});
|
||||
|
||||
// chmod is a no-op on Windows — 0o600 can never be observed there.
|
||||
it.skipIf(IS_WINDOWS)("hardenBackupPermissions sets 0o600 on all backup files", async () => {
|
||||
await withTempHome(async () => {
|
||||
const configPath = resolveConfigPathFromTempState();
|
||||
|
||||
// Create .bak and .bak.1 with permissive mode
|
||||
await fs.writeFile(`${configPath}.bak`, "secret", { mode: 0o644 });
|
||||
await fs.writeFile(`${configPath}.bak.1`, "secret", { mode: 0o644 });
|
||||
|
||||
await hardenBackupPermissions(configPath, fs);
|
||||
|
||||
const bakStat = await fs.stat(`${configPath}.bak`);
|
||||
const bak1Stat = await fs.stat(`${configPath}.bak.1`);
|
||||
|
||||
expectPosixMode(bakStat.mode, 0o600);
|
||||
expectPosixMode(bak1Stat.mode, 0o600);
|
||||
});
|
||||
});
|
||||
|
||||
it("cleanOrphanBackups removes stale files outside the rotation ring", async () => {
|
||||
await withTempHome(async () => {
|
||||
const configPath = resolveConfigPathFromTempState();
|
||||
|
||||
// Create valid backups
|
||||
await fs.writeFile(configPath, "current");
|
||||
await fs.writeFile(`${configPath}.bak`, "backup-0");
|
||||
await fs.writeFile(`${configPath}.bak.1`, "backup-1");
|
||||
await fs.writeFile(`${configPath}.bak.2`, "backup-2");
|
||||
|
||||
// Create orphans
|
||||
await fs.writeFile(`${configPath}.bak.1772352289`, "orphan-pid");
|
||||
await fs.writeFile(`${configPath}.bak.before-marketing`, "orphan-manual");
|
||||
await fs.writeFile(`${configPath}.bak.99`, "orphan-overflow");
|
||||
|
||||
await cleanOrphanBackups(configPath, fs);
|
||||
|
||||
// Valid backups preserved
|
||||
await expectRegularFile(`${configPath}.bak`);
|
||||
await expectRegularFile(`${configPath}.bak.1`);
|
||||
await expectRegularFile(`${configPath}.bak.2`);
|
||||
|
||||
// Orphans removed
|
||||
await expectPathMissing(`${configPath}.bak.1772352289`);
|
||||
await expectPathMissing(`${configPath}.bak.before-marketing`);
|
||||
await expectPathMissing(`${configPath}.bak.99`);
|
||||
|
||||
// Main config untouched
|
||||
await expect(fs.readFile(configPath, "utf-8")).resolves.toBe("current");
|
||||
await expect(readVersion(".pre-update")).resolves.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,31 +104,6 @@ describe("config backup rotation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("createPreUpdateConfigSnapshot survives multiple config writes", async () => {
|
||||
await withTempHome(async () => {
|
||||
const configPath = resolveConfigPathFromTempState();
|
||||
const original = JSON.stringify({ version: "original" });
|
||||
await fs.writeFile(configPath, original, { mode: 0o600 });
|
||||
|
||||
const { existsSync } = await import("node:fs");
|
||||
await createPreUpdateConfigSnapshot({
|
||||
configPath,
|
||||
fs: { writeFile: fs.writeFile, readFile: fs.readFile, existsSync },
|
||||
});
|
||||
|
||||
// Simulate multiple config writes + backup rotations
|
||||
for (let i = 0; i < 7; i++) {
|
||||
await rotateConfigBackups(configPath, fs);
|
||||
await fs.copyFile(configPath, `${configPath}.bak`);
|
||||
await fs.writeFile(configPath, JSON.stringify({ version: `write-${i}` }));
|
||||
}
|
||||
|
||||
// .pre-update still holds the original content
|
||||
const snapshotPath = `${configPath}.pre-update`;
|
||||
await expect(fs.readFile(snapshotPath, "utf-8")).resolves.toBe(original);
|
||||
});
|
||||
});
|
||||
|
||||
it("createPreUpdateConfigSnapshot replaces a preexisting snapshot once per process", async () => {
|
||||
await withTempHome(async () => {
|
||||
const configPath = resolveConfigPathFromTempState();
|
||||
@@ -261,7 +170,6 @@ describe("config backup rotation", () => {
|
||||
});
|
||||
await expectPathMissing(`${configPath}.pre-update`);
|
||||
|
||||
// The failed attempt must not suppress the next snapshot pass.
|
||||
await createPreUpdateConfigSnapshot({
|
||||
configPath,
|
||||
fs: { writeFile: fs.writeFile, readFile: fs.readFile, existsSync },
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "./config-env-vars.js";
|
||||
import { resolveConfigEnvVars } from "./env-substitution.js";
|
||||
import { assertGatewayConfigEnvSelectionUnchanged } from "./gateway-env-selection.js";
|
||||
import { collectDurableServiceEnvVars, readStateDirDotEnvVars } from "./state-dir-dotenv.js";
|
||||
import { collectDurableServiceEnvVars } from "./state-dir-dotenv.js";
|
||||
import { withEnvOverride, withTempHome, writeStateDirDotEnv } from "./test-helpers.js";
|
||||
import type { OpenClawConfig } from "./types.js";
|
||||
|
||||
@@ -598,7 +598,7 @@ describe("config env vars", () => {
|
||||
await writeStateDirDotEnv("BRAVE_API_KEY=BSA-test-key\nDISCORD_BOT_TOKEN=discord-tok\n", {
|
||||
env: process.env,
|
||||
});
|
||||
const vars = readStateDirDotEnvVars(process.env);
|
||||
const vars = collectDurableServiceEnvVars({ env: process.env });
|
||||
expect(vars.BRAVE_API_KEY).toBe("BSA-test-key");
|
||||
expect(vars.DISCORD_BOT_TOKEN).toBe("discord-tok");
|
||||
});
|
||||
@@ -606,7 +606,7 @@ describe("config env vars", () => {
|
||||
|
||||
it("returns empty record when the state-dir .env file is missing", async () => {
|
||||
await withTempHome(async (_home) => {
|
||||
expect(readStateDirDotEnvVars(process.env)).toStrictEqual({});
|
||||
expect(collectDurableServiceEnvVars({ env: process.env })).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -616,7 +616,7 @@ describe("config env vars", () => {
|
||||
"NODE_OPTIONS=--require /tmp/evil.js\nOPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS=1\nEMPTY=\nVALID=ok\n",
|
||||
{ env: process.env },
|
||||
);
|
||||
const vars = readStateDirDotEnvVars(process.env);
|
||||
const vars = collectDurableServiceEnvVars({ env: process.env });
|
||||
expect(vars.NODE_OPTIONS).toBeUndefined();
|
||||
expect(vars.OPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS).toBeUndefined();
|
||||
expect(vars.EMPTY).toBeUndefined();
|
||||
@@ -631,8 +631,8 @@ describe("config env vars", () => {
|
||||
stateDir: customStateDir,
|
||||
});
|
||||
expect(
|
||||
readStateDirDotEnvVars({
|
||||
OPENCLAW_STATE_DIR: customStateDir,
|
||||
collectDurableServiceEnvVars({
|
||||
env: { OPENCLAW_STATE_DIR: customStateDir },
|
||||
}).CUSTOM_KEY,
|
||||
).toBe("from-override");
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { CronConfig } from "./types.cron.js";
|
||||
|
||||
/** Default maximum number of cron jobs allowed to run at once. */
|
||||
export const DEFAULT_CRON_MAX_CONCURRENT_RUNS = 8;
|
||||
export const DEFAULT_CRON_TRIGGER_MIN_INTERVAL_MS = 30_000;
|
||||
const DEFAULT_CRON_TRIGGER_MIN_INTERVAL_MS = 30_000;
|
||||
|
||||
/** Resolves cron concurrency config, flooring finite values and clamping to at least one. */
|
||||
export function resolveCronMaxConcurrentRuns(
|
||||
|
||||
@@ -5,4 +5,3 @@ export {
|
||||
createConfigRuntimeEnv,
|
||||
isConfigRuntimeEnvVarAllowed,
|
||||
} from "./config-env-vars.js";
|
||||
export { collectDurableServiceEnvVars, readStateDirDotEnvVars } from "./state-dir-dotenv.js";
|
||||
|
||||
+33
-37
@@ -9,9 +9,6 @@ import {
|
||||
CircularIncludeError,
|
||||
ConfigIncludeError,
|
||||
MAX_INCLUDE_DEPTH,
|
||||
MAX_INCLUDE_FILE_BYTES,
|
||||
MAX_INCLUDE_PATH_LENGTH,
|
||||
deepMerge,
|
||||
type IncludeResolver,
|
||||
resolveConfigIncludeWritePath,
|
||||
resolveConfigIncludes,
|
||||
@@ -722,30 +719,24 @@ describe("security: path traversal protection (CWE-22)", () => {
|
||||
});
|
||||
|
||||
describe("prototype pollution protection", () => {
|
||||
it("blocks prototype pollution vectors in shallow and nested merges", () => {
|
||||
const cases = [
|
||||
{
|
||||
base: {},
|
||||
incoming: JSON.parse('{"__proto__":{"polluted":true}}'),
|
||||
expected: {},
|
||||
},
|
||||
{
|
||||
base: { safe: 1 },
|
||||
incoming: { prototype: { x: 1 }, constructor: { y: 2 }, normal: 3 },
|
||||
expected: { safe: 1, normal: 3 },
|
||||
},
|
||||
{
|
||||
base: { nested: { a: 1 } },
|
||||
incoming: { nested: JSON.parse('{"__proto__":{"polluted":true}}') },
|
||||
expected: { nested: { a: 1 } },
|
||||
},
|
||||
] as const;
|
||||
it("blocks prototype pollution vectors in included and sibling config", () => {
|
||||
const includePath = configPath("pollution.json");
|
||||
const included = JSON.parse(
|
||||
'{"__proto__":{"polluted":true},"constructor":{"hidden":true},"normal":3}',
|
||||
) as Record<string, unknown>;
|
||||
const sibling = JSON.parse('{"__proto__":{"alsoPolluted":true},"safe":1}') as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
for (const { base, incoming, expected } of cases) {
|
||||
const result = deepMerge(base, incoming);
|
||||
expect((Object.prototype as Record<string, unknown>).polluted).toBeUndefined();
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
const result = resolve(
|
||||
{ $include: "./pollution.json", ...sibling },
|
||||
{ [includePath]: included },
|
||||
);
|
||||
|
||||
expect((Object.prototype as Record<string, unknown>).polluted).toBeUndefined();
|
||||
expect((Object.prototype as Record<string, unknown>).alsoPolluted).toBeUndefined();
|
||||
expect(result).toEqual({ normal: 3, safe: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -762,12 +753,12 @@ describe("security: path traversal protection (CWE-22)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects include path at or over maximum length (>= MAX_INCLUDE_PATH_LENGTH)", () => {
|
||||
const overLimit = "a".repeat(MAX_INCLUDE_PATH_LENGTH + 1);
|
||||
expectResolveIncludeError(() => resolve({ $include: overLimit }, {}), /maximum length/);
|
||||
// Boundary: length exactly 4096 must be rejected (Linux PATH_MAX includes NUL)
|
||||
const atLimit = "b".repeat(MAX_INCLUDE_PATH_LENGTH);
|
||||
expectResolveIncludeError(() => resolve({ $include: atLimit }, {}), /maximum length/);
|
||||
it("rejects include paths at or over the platform-safe maximum", () => {
|
||||
expectResolveIncludeError(() => resolve({ $include: "a".repeat(4096) }, {}), /maximum length/);
|
||||
expectResolveIncludeError(
|
||||
() => resolve({ $include: "b".repeat(4097) }, {}),
|
||||
/maximum length/,
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts include path at or under maximum length when file exists", () => {
|
||||
@@ -861,16 +852,21 @@ describe("security: path traversal protection (CWE-22)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects oversized include files", async () => {
|
||||
it("rejects include files larger than the guarded read limit", async () => {
|
||||
await withTempDir({ prefix: "openclaw-includes-big-" }, async (tempRoot) => {
|
||||
const configDir = path.join(tempRoot, "config");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
const includePath = path.join(configDir, "big.json5");
|
||||
const payload = "a".repeat(MAX_INCLUDE_FILE_BYTES + 1);
|
||||
await fs.writeFile(includePath, `{"blob":"${payload}"}`, "utf-8");
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "big.json5"),
|
||||
`{"blob":"${"a".repeat(2 * 1024 * 1024 + 1)}"}`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
resolveConfigIncludes({ $include: "./big.json5" }, path.join(configDir, "openclaw.json")),
|
||||
resolveConfigIncludes(
|
||||
{ $include: "./big.json5" },
|
||||
path.join(configDir, "openclaw.json"),
|
||||
),
|
||||
).toThrow(/security checks|max/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,10 +23,10 @@ import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js";
|
||||
|
||||
export const INCLUDE_KEY = "$include";
|
||||
export const MAX_INCLUDE_DEPTH = 10;
|
||||
export const MAX_INCLUDE_FILE_BYTES = 2 * 1024 * 1024;
|
||||
const MAX_INCLUDE_FILE_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
/** Maximum length for $include path and resolved path (CWE-22 hardening). */
|
||||
export const MAX_INCLUDE_PATH_LENGTH = 4096;
|
||||
const MAX_INCLUDE_PATH_LENGTH = 4096;
|
||||
|
||||
export function hashConfigIncludeRaw(raw: string | null): string {
|
||||
const hash = crypto.createHash("sha256");
|
||||
@@ -136,7 +136,7 @@ export class CircularIncludeError extends ConfigIncludeError {
|
||||
// ============================================================================
|
||||
|
||||
/** Deep merge: arrays concatenate, objects merge recursively, primitives: source wins */
|
||||
export function deepMerge(target: unknown, source: unknown): unknown {
|
||||
function deepMerge(target: unknown, source: unknown): unknown {
|
||||
return mergeDeepValues(target, source, { arrays: "concat", undefinedValues: "replace" });
|
||||
}
|
||||
|
||||
|
||||
+42
-161
@@ -9,12 +9,11 @@ import {
|
||||
createConfigWriteAuditRecordBase,
|
||||
finalizeConfigWriteAuditRecord,
|
||||
formatConfigOverwriteLogMessage,
|
||||
redactConfigAuditArgv,
|
||||
resolveConfigAuditLogPath,
|
||||
scrubConfigAuditLog,
|
||||
} from "./io.audit.js";
|
||||
|
||||
function createAuditRecordBase(configPath: string) {
|
||||
function createAuditRecordBase(configPath: string, argv?: string[]) {
|
||||
return createConfigWriteAuditRecordBase({
|
||||
configPath,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
@@ -38,6 +37,17 @@ function createAuditRecordBase(configPath: string) {
|
||||
gatewayModeAfter: "local",
|
||||
suspicious: [],
|
||||
now: "2026-04-07T08:00:00.000Z",
|
||||
...(argv
|
||||
? {
|
||||
processInfo: {
|
||||
pid: 101,
|
||||
ppid: 99,
|
||||
cwd: "/work",
|
||||
argv,
|
||||
execArgv: [],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -235,165 +245,6 @@ describe("config io audit helpers", () => {
|
||||
expect(raw).not.toContain("abcd-efgh-ijkl-mnop");
|
||||
});
|
||||
|
||||
it("redacts argv values that follow known secret flag names", () => {
|
||||
const argv = [
|
||||
"node",
|
||||
"openclaw",
|
||||
"gateway",
|
||||
"--token",
|
||||
"super-secret-gateway-token-12345",
|
||||
"--api-key",
|
||||
"sk-very-real-looking-openai-api-key-AB12CD34",
|
||||
"--port",
|
||||
"8080",
|
||||
];
|
||||
const result = redactConfigAuditArgv(argv);
|
||||
expect(result).toEqual([
|
||||
"node",
|
||||
"openclaw",
|
||||
"gateway",
|
||||
"--token",
|
||||
"***",
|
||||
"--api-key",
|
||||
"***",
|
||||
"--port",
|
||||
"8080",
|
||||
]);
|
||||
});
|
||||
|
||||
it("redacts the value half of `--flag=value` for secret flags", () => {
|
||||
const argv = ["openclaw", "--token=ghp_realgithubtoken1234567890ABCD", "--port=8080"];
|
||||
expect(redactConfigAuditArgv(argv)).toEqual(["openclaw", "--token=***", "--port=8080"]);
|
||||
});
|
||||
|
||||
it("redacts standalone token shapes via the shared logging redaction patterns", () => {
|
||||
const argv = [
|
||||
"node",
|
||||
"openclaw",
|
||||
"ghp_realgithubtoken1234567890ABCD",
|
||||
"AIzaSyD-very-real-looking-google-api-key-123",
|
||||
"987654321:AAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
];
|
||||
const result = redactConfigAuditArgv(argv);
|
||||
expect(result[0]).toBe("node");
|
||||
expect(result[1]).toBe("openclaw");
|
||||
for (const masked of result.slice(2)) {
|
||||
expect(masked).not.toContain("ghp_realgithubtoken");
|
||||
expect(masked).not.toContain("AIzaSyD-very-real-looking");
|
||||
expect(masked).not.toMatch(/AAAAAAAAAAAAAA/);
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves non-secret arguments untouched", () => {
|
||||
const argv = ["node", "openclaw", "gateway", "--port", "8080", "--bind", "lan"];
|
||||
expect(redactConfigAuditArgv(argv)).toEqual(argv);
|
||||
});
|
||||
|
||||
it("redacts unknown but credential-suffixed flags via the heuristic classifier", () => {
|
||||
const argv = [
|
||||
"node",
|
||||
"openclaw",
|
||||
"--custom-api-key",
|
||||
"real-tenant-key-AB12CD34EF56GH78",
|
||||
"--alibaba-model-studio-api-key=plain-value-xyz-12345",
|
||||
"--app-token",
|
||||
"another-secret-value",
|
||||
"--frobnicate-credential=hidden",
|
||||
];
|
||||
const result = redactConfigAuditArgv(argv);
|
||||
expect(result).toEqual([
|
||||
"node",
|
||||
"openclaw",
|
||||
"--custom-api-key",
|
||||
"***",
|
||||
"--alibaba-model-studio-api-key=***",
|
||||
"--app-token",
|
||||
"***",
|
||||
"--frobnicate-credential=***",
|
||||
]);
|
||||
});
|
||||
|
||||
it("redacts key-valued secret flags (Nostr --private-key, Matrix --recovery-key)", () => {
|
||||
const argv = [
|
||||
"node",
|
||||
"openclaw",
|
||||
"channels",
|
||||
"add",
|
||||
"--channel",
|
||||
"nostr",
|
||||
"--private-key",
|
||||
"nsec1realnostrprivatekeyvaluexyz1234567890",
|
||||
"--recovery-key=EsTb-ABCD-1234-EFGH-5678-IJKL-9012-MNOP",
|
||||
];
|
||||
const result = redactConfigAuditArgv(argv);
|
||||
expect(result).toEqual([
|
||||
"node",
|
||||
"openclaw",
|
||||
"channels",
|
||||
"add",
|
||||
"--channel",
|
||||
"nostr",
|
||||
"--private-key",
|
||||
"***",
|
||||
"--recovery-key=***",
|
||||
]);
|
||||
});
|
||||
|
||||
it("redacts unknown *-key flags via the heuristic classifier (private/signing/master/etc.)", () => {
|
||||
const argv = [
|
||||
"node",
|
||||
"openclaw",
|
||||
"--my-plugin-private-key",
|
||||
"tenant-private-key-material-zzz",
|
||||
"--rotated-signing-key=PEM-LIKE-MATERIAL",
|
||||
"--ops-master-key",
|
||||
"ABCDEF1234567890",
|
||||
"--secret-key",
|
||||
"opaque-secret-key",
|
||||
"--aws-secret-access-key=opaque-aws-secret",
|
||||
"--openai_api_key",
|
||||
"opaque-underscore-key",
|
||||
"--aws_secret_access_key=opaque-underscore-aws-secret",
|
||||
"--credentials",
|
||||
"opaque-plural-credential",
|
||||
"--service-credentials=opaque-inline-plural-credential",
|
||||
];
|
||||
const result = redactConfigAuditArgv(argv);
|
||||
expect(result).toEqual([
|
||||
"node",
|
||||
"openclaw",
|
||||
"--my-plugin-private-key",
|
||||
"***",
|
||||
"--rotated-signing-key=***",
|
||||
"--ops-master-key",
|
||||
"***",
|
||||
"--secret-key",
|
||||
"***",
|
||||
"--aws-secret-access-key=***",
|
||||
"--openai_api_key",
|
||||
"***",
|
||||
"--aws_secret_access_key=***",
|
||||
"--credentials",
|
||||
"***",
|
||||
"--service-credentials=***",
|
||||
]);
|
||||
});
|
||||
|
||||
it("masks the next arg after a secret flag even when it looks like another option", () => {
|
||||
const argv = ["openclaw", "--token", "--port", "8080"];
|
||||
expect(redactConfigAuditArgv(argv)).toEqual(["openclaw", "--token", "***", "8080"]);
|
||||
});
|
||||
|
||||
it("redacts dash-leading secret values after bare secret flags", () => {
|
||||
const argv = ["openclaw", "--password", "-secret-value"];
|
||||
expect(redactConfigAuditArgv(argv)).toEqual(["openclaw", "--password", "***"]);
|
||||
});
|
||||
|
||||
it("does not mask when a secret flag is the final arg with no value", () => {
|
||||
const argv = ["openclaw", "--token"];
|
||||
expect(redactConfigAuditArgv(argv)).toEqual(["openclaw", "--token"]);
|
||||
});
|
||||
|
||||
it("caps caller-supplied processInfo argv at 8 entries before redaction", () => {
|
||||
const longArgv = [
|
||||
"node",
|
||||
@@ -478,6 +329,36 @@ describe("config io audit helpers", () => {
|
||||
expect(base.argv).toEqual(["node", "openclaw", "--token", "***"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "inline known secret",
|
||||
argv: ["openclaw", "--token=fake", "--port=8080"],
|
||||
expected: ["openclaw", "--token=***", "--port=8080"],
|
||||
},
|
||||
{
|
||||
name: "custom credential suffix",
|
||||
argv: ["openclaw", "--tenant-credential", "fake", "--bind", "lan"],
|
||||
expected: ["openclaw", "--tenant-credential", "***", "--bind", "lan"],
|
||||
},
|
||||
{
|
||||
name: "underscore key suffix",
|
||||
argv: ["openclaw", "--provider_api_key", "fake"],
|
||||
expected: ["openclaw", "--provider_api_key", "***"],
|
||||
},
|
||||
{
|
||||
name: "dash-leading secret value",
|
||||
argv: ["openclaw", "--password", "-fake"],
|
||||
expected: ["openclaw", "--password", "***"],
|
||||
},
|
||||
{
|
||||
name: "secret flag without a value",
|
||||
argv: ["openclaw", "--token"],
|
||||
expected: ["openclaw", "--token"],
|
||||
},
|
||||
])("redacts $name in persisted audit process info", ({ argv, expected }) => {
|
||||
expect(createAuditRecordBase("/tmp/openclaw.json", argv).argv).toEqual(expected);
|
||||
});
|
||||
|
||||
it("also accepts flattened audit record params from legacy call sites", async () => {
|
||||
const home = await suiteRootTracker.make("append-flat");
|
||||
const record = createRenameAuditRecord(home);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { redactSensitiveArgv } from "./redact-argv.js";
|
||||
|
||||
const CONFIG_AUDIT_ARGV_CAP = 8;
|
||||
|
||||
export function redactConfigAuditArgv(argv: readonly string[]): string[] {
|
||||
function redactConfigAuditArgv(argv: readonly string[]): string[] {
|
||||
return redactSensitiveArgv(argv);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export type ConfigHealthState = {
|
||||
|
||||
type ConfigHealthDatabase = Pick<OpenClawStateKyselyDatabase, "config_health_entries">;
|
||||
|
||||
export type ConfigHealthStateDeps = {
|
||||
type ConfigHealthStateDeps = {
|
||||
env: NodeJS.ProcessEnv;
|
||||
homedir: () => string;
|
||||
logger: Pick<typeof console, "warn">;
|
||||
|
||||
@@ -3,9 +3,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createInvalidConfigError,
|
||||
formatInvalidConfigDetails,
|
||||
formatInvalidConfigLogMessage,
|
||||
isInvalidConfigError,
|
||||
logInvalidConfigOnce,
|
||||
throwInvalidConfig,
|
||||
} from "./io.invalid-config.js";
|
||||
|
||||
@@ -28,12 +26,6 @@ describe("config io invalid config formatting", () => {
|
||||
expect(details).toContain("- <root>: root problem");
|
||||
});
|
||||
|
||||
it("formats the logger message with the escaped newline separator", () => {
|
||||
expect(formatInvalidConfigLogMessage("/tmp/openclaw.json", "- gateway.port: bad")).toBe(
|
||||
"Invalid config at /tmp/openclaw.json:\\n- gateway.port: bad",
|
||||
);
|
||||
});
|
||||
|
||||
it("creates INVALID_CONFIG errors with inline details", () => {
|
||||
const err = createInvalidConfigError("/tmp/openclaw.json", "- gateway.port: bad") as Error & {
|
||||
code?: string;
|
||||
@@ -51,40 +43,26 @@ describe("config io invalid config formatting", () => {
|
||||
expect(isInvalidConfigError(new Error(err.message))).toBe(false);
|
||||
});
|
||||
|
||||
it("logs invalid config details only once per path", () => {
|
||||
it("throws INVALID_CONFIG after logging the formatted details once per path", () => {
|
||||
const logger = { error: vi.fn() };
|
||||
const loggedConfigPaths = new Set<string>();
|
||||
|
||||
logInvalidConfigOnce({
|
||||
configPath: "/tmp/openclaw.json",
|
||||
details: "- gateway.port: bad",
|
||||
logger,
|
||||
loggedConfigPaths,
|
||||
});
|
||||
logInvalidConfigOnce({
|
||||
configPath: "/tmp/openclaw.json",
|
||||
details: "- gateway.port: worse",
|
||||
logger,
|
||||
loggedConfigPaths,
|
||||
});
|
||||
|
||||
expect(logger.error).toHaveBeenCalledOnce();
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"Invalid config at /tmp/openclaw.json:\\n- gateway.port: bad",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws INVALID_CONFIG after logging the formatted details", () => {
|
||||
const logger = { error: vi.fn() };
|
||||
|
||||
expect(() =>
|
||||
const throwInvalid = () =>
|
||||
throwInvalidConfig({
|
||||
configPath: "/tmp/openclaw.json",
|
||||
issues: [{ path: "nope", message: "Unknown key(s): nope" }],
|
||||
logger,
|
||||
loggedConfigPaths: new Set<string>(),
|
||||
}),
|
||||
).toThrowError("Invalid config at /tmp/openclaw.json:\n- nope: Unknown key(s): nope");
|
||||
loggedConfigPaths,
|
||||
});
|
||||
|
||||
expect(throwInvalid).toThrowError(
|
||||
"Invalid config at /tmp/openclaw.json:\n- nope: Unknown key(s): nope",
|
||||
);
|
||||
expect(throwInvalid).toThrowError(
|
||||
"Invalid config at /tmp/openclaw.json:\n- nope: Unknown key(s): nope",
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledOnce();
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"Invalid config at /tmp/openclaw.json:\\n- nope: Unknown key(s): nope",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,12 +23,12 @@ export function formatInvalidConfigDetails(issues: ConfigValidationIssueLike[]):
|
||||
}
|
||||
|
||||
/** Builds the one-line invalid-config prefix plus preformatted validation details. */
|
||||
export function formatInvalidConfigLogMessage(configPath: string, details: string): string {
|
||||
function formatInvalidConfigLogMessage(configPath: string, details: string): string {
|
||||
return `Invalid config at ${configPath}:\\n${details}`;
|
||||
}
|
||||
|
||||
/** Logs an invalid config message once per path during a load sequence. */
|
||||
export function logInvalidConfigOnce(params: {
|
||||
function logInvalidConfigOnce(params: {
|
||||
configPath: string;
|
||||
details: string;
|
||||
logger: Pick<typeof console, "error">;
|
||||
|
||||
@@ -17,13 +17,16 @@ import {
|
||||
maybeRecoverSuspiciousConfigReadSync,
|
||||
promoteConfigSnapshotToLastKnownGood,
|
||||
recoverConfigFromLastKnownGood,
|
||||
resolveLastKnownGoodConfigPath,
|
||||
type ObserveRecoveryDeps,
|
||||
} from "./io.observe-recovery.js";
|
||||
import type { ConfigFileSnapshot } from "./types.js";
|
||||
|
||||
const CONFIG_CLOBBER_SNAPSHOT_LIMIT = 32;
|
||||
type ConfigHealthDatabase = Pick<OpenClawStateKyselyDatabase, "config_health_entries">;
|
||||
type ObserveRecoveryDeps = Parameters<typeof maybeRecoverSuspiciousConfigRead>[0]["deps"];
|
||||
|
||||
function resolveLastKnownGoodConfigPath(configPath: string): string {
|
||||
return `${configPath}.last-good`;
|
||||
}
|
||||
|
||||
describe("config observe recovery", () => {
|
||||
let fixtureRoot = "";
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
import type { ConfigFileSnapshot } from "./types.openclaw.js";
|
||||
|
||||
/** Dependencies injected into config recovery observation for testable filesystem behavior. */
|
||||
export type ObserveRecoveryDeps = {
|
||||
type ObserveRecoveryDeps = {
|
||||
fs: {
|
||||
promises: {
|
||||
stat(path: string): Promise<{
|
||||
@@ -570,7 +570,7 @@ function readConfigFingerprintForPathSync(
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveLastKnownGoodConfigPath(configPath: string): string {
|
||||
function resolveLastKnownGoodConfigPath(configPath: string): string {
|
||||
return `${configPath}.last-good`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isRecord } from "../utils.js";
|
||||
|
||||
export type ConfigObserveSuspiciousBaseline = {
|
||||
type ConfigObserveSuspiciousBaseline = {
|
||||
bytes: number;
|
||||
hasMeta: boolean;
|
||||
gatewayMode: string | null;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// Verifies owner display secrets stay redacted in config IO paths.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
type OwnerDisplaySecretRuntimeState,
|
||||
retainGeneratedOwnerDisplaySecret,
|
||||
} from "./io.owner-display-secret.js";
|
||||
import { retainGeneratedOwnerDisplaySecret } from "./io.owner-display-secret.js";
|
||||
import type { OpenClawConfig } from "./types.openclaw.js";
|
||||
|
||||
type OwnerDisplaySecretRuntimeState = Parameters<
|
||||
typeof retainGeneratedOwnerDisplaySecret
|
||||
>[0]["state"];
|
||||
|
||||
function createState(): OwnerDisplaySecretRuntimeState {
|
||||
return {
|
||||
pendingByPath: new Map<string, string>(),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import type { OpenClawConfig } from "./types.openclaw.js";
|
||||
|
||||
/** Runtime-only owner display secrets keyed by config path during config IO. */
|
||||
export type OwnerDisplaySecretRuntimeState = {
|
||||
type OwnerDisplaySecretRuntimeState = {
|
||||
pendingByPath: Map<string, string>;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
restoreEnvRefsFromMap,
|
||||
resolvePersistCandidateForWrite,
|
||||
resolveWriteEnvSnapshotForPath,
|
||||
unsetPathForWrite,
|
||||
} from "./io.write-prepare.js";
|
||||
import type { OpenClawConfig } from "./types.js";
|
||||
|
||||
@@ -504,6 +503,46 @@ describe("config io write prepare", () => {
|
||||
expect(persisted.agents?.defaults?.models?.["openai/gpt-5.4"]).not.toHaveProperty("params");
|
||||
});
|
||||
|
||||
it("applies explicit unsets without mutating caller config", () => {
|
||||
const input: OpenClawConfig = {
|
||||
gateway: { mode: "local" },
|
||||
commands: { ownerDisplay: "hash" },
|
||||
tools: { alsoAllow: ["exec", "fetch", "read"] },
|
||||
};
|
||||
|
||||
const next = applyUnsetPathsForWrite(input, [
|
||||
["commands", "ownerDisplay"],
|
||||
["tools", "alsoAllow", "1"],
|
||||
]);
|
||||
|
||||
expect(input).toEqual({
|
||||
gateway: { mode: "local" },
|
||||
commands: { ownerDisplay: "hash" },
|
||||
tools: { alsoAllow: ["exec", "fetch", "read"] },
|
||||
});
|
||||
expect(next.commands ?? {}).not.toHaveProperty("ownerDisplay");
|
||||
expect(next.tools?.alsoAllow).toEqual(["exec", "read"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["invalid array suffix", ["tools", "alsoAllow", "1abc"]],
|
||||
["signed array index", ["tools", "alsoAllow", "+0"]],
|
||||
["unsafe integer", ["tools", "alsoAllow", "9007199254740993"]],
|
||||
["maximum array key", ["tools", "alsoAllow", "4294967294"]],
|
||||
["missing key", ["commands", "missingKey"]],
|
||||
["prototype key", ["commands", "__proto__"]],
|
||||
["constructor key", ["commands", "constructor"]],
|
||||
["prototype constructor property", ["commands", "prototype"]],
|
||||
] as const)("treats %s unset paths as immutable no-ops", (_name, unsetPath) => {
|
||||
const input: OpenClawConfig = {
|
||||
gateway: { mode: "local" },
|
||||
commands: { ownerDisplay: "hash" },
|
||||
tools: { alsoAllow: ["exec", "fetch"] },
|
||||
};
|
||||
|
||||
expect(applyUnsetPathsForWrite(input, [[...unsetPath]])).toBe(input);
|
||||
});
|
||||
|
||||
it("preserves untouched include-owned subtrees during unrelated writes", () => {
|
||||
const persisted = resolvePersistCandidateForWrite({
|
||||
runtimeConfig: {
|
||||
@@ -822,105 +861,6 @@ describe("config io write prepare", () => {
|
||||
expect(message).toContain('openclaw config set channels.telegram.dmPolicy "pairing"');
|
||||
});
|
||||
|
||||
it("unsets explicit paths when runtime defaults would otherwise reappear", () => {
|
||||
const next = unsetPathForWrite(
|
||||
{
|
||||
gateway: { auth: { mode: "none" } },
|
||||
commands: { ownerDisplay: "hash" },
|
||||
},
|
||||
["commands", "ownerDisplay"],
|
||||
);
|
||||
|
||||
expect(next.changed).toBe(true);
|
||||
expect(next.next.commands ?? {}).not.toHaveProperty("ownerDisplay");
|
||||
});
|
||||
|
||||
it("does not mutate caller config when unsetting existing config objects", () => {
|
||||
const input: OpenClawConfig = {
|
||||
gateway: { mode: "local" },
|
||||
commands: { ownerDisplay: "hash" },
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const next = unsetPathForWrite(input, ["commands", "ownerDisplay"]);
|
||||
|
||||
expect(input).toEqual({
|
||||
gateway: { mode: "local" },
|
||||
commands: { ownerDisplay: "hash" },
|
||||
});
|
||||
expect(next.next.commands ?? {}).not.toHaveProperty("ownerDisplay");
|
||||
});
|
||||
|
||||
it("keeps caller arrays immutable when unsetting array entries", () => {
|
||||
const input: OpenClawConfig = {
|
||||
gateway: { mode: "local" },
|
||||
tools: { alsoAllow: ["exec", "fetch", "read"] },
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const next = unsetPathForWrite(input, ["tools", "alsoAllow", "1"]);
|
||||
|
||||
expect(input.tools!.alsoAllow).toEqual(["exec", "fetch", "read"]);
|
||||
expect((next.next.tools as { alsoAllow?: string[] } | undefined)?.alsoAllow).toEqual([
|
||||
"exec",
|
||||
"read",
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats invalid array-index unset paths as no-ops", () => {
|
||||
const input: OpenClawConfig = {
|
||||
gateway: { mode: "local" },
|
||||
tools: { alsoAllow: ["exec", "fetch"] },
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
for (const path of [
|
||||
["tools", "alsoAllow", "1abc"],
|
||||
["tools", "alsoAllow", "+0"],
|
||||
["tools", "alsoAllow", "9007199254740993"],
|
||||
["tools", "alsoAllow", "4294967294"],
|
||||
]) {
|
||||
const next = unsetPathForWrite(input, path);
|
||||
expect(next.changed).toBe(false);
|
||||
expect(next.next).toBe(input);
|
||||
}
|
||||
});
|
||||
|
||||
it("treats missing unset paths as no-op without mutating caller config", () => {
|
||||
const input: OpenClawConfig = {
|
||||
gateway: { mode: "local" },
|
||||
commands: { ownerDisplay: "hash" },
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const next = unsetPathForWrite(input, ["commands", "missingKey"]);
|
||||
|
||||
expect(next.changed).toBe(false);
|
||||
expect(next.next).toBe(input);
|
||||
expect(input).toEqual({
|
||||
gateway: { mode: "local" },
|
||||
commands: { ownerDisplay: "hash" },
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores blocked prototype-key unset path segments", () => {
|
||||
const input: OpenClawConfig = {
|
||||
gateway: { mode: "local" },
|
||||
commands: { ownerDisplay: "hash" },
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const blocked = [
|
||||
["commands", "__proto__"],
|
||||
["commands", "constructor"],
|
||||
["commands", "prototype"],
|
||||
].map((segments) => unsetPathForWrite(input, segments));
|
||||
|
||||
for (const result of blocked) {
|
||||
expect(result.changed).toBe(false);
|
||||
expect(result.next).toBe(input);
|
||||
}
|
||||
expect(input).toEqual({
|
||||
gateway: { mode: "local" },
|
||||
commands: { ownerDisplay: "hash" },
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves env refs on unchanged paths while keeping changed paths resolved", () => {
|
||||
const changedPaths = new Set<string>();
|
||||
collectChangedPaths(
|
||||
|
||||
@@ -1082,7 +1082,7 @@ function unsetPathForWriteAt(
|
||||
};
|
||||
}
|
||||
|
||||
export function unsetPathForWrite(
|
||||
function unsetPathForWrite(
|
||||
root: OpenClawConfig,
|
||||
pathSegments: string[],
|
||||
): { changed: boolean; next: OpenClawConfig } {
|
||||
|
||||
@@ -4,19 +4,10 @@ import {
|
||||
formatConfigIssueLine,
|
||||
formatConfigIssueLines,
|
||||
formatConfigIssueSummary,
|
||||
normalizeConfigIssue,
|
||||
normalizeConfigIssuePath,
|
||||
normalizeConfigIssues,
|
||||
} from "./issue-format.js";
|
||||
|
||||
describe("config issue format", () => {
|
||||
it("normalizes empty paths to <root>", () => {
|
||||
expect(normalizeConfigIssuePath("")).toBe("<root>");
|
||||
expect(normalizeConfigIssuePath(" ")).toBe("<root>");
|
||||
expect(normalizeConfigIssuePath(null)).toBe("<root>");
|
||||
expect(normalizeConfigIssuePath(undefined)).toBe("<root>");
|
||||
});
|
||||
|
||||
it("formats issue lines with and without markers", () => {
|
||||
expect(formatConfigIssueLine({ path: "", message: "broken" }, "-")).toBe("- : broken");
|
||||
expect(
|
||||
@@ -63,20 +54,7 @@ describe("config issue format", () => {
|
||||
).toBe("<root>: root broken; gateway.auth.password.source: Required; and 1 more");
|
||||
});
|
||||
|
||||
it("normalizes issue metadata for machine output", () => {
|
||||
expect(
|
||||
normalizeConfigIssue({
|
||||
path: "",
|
||||
message: "invalid",
|
||||
allowedValues: ["stable", "beta"],
|
||||
allowedValuesHiddenCount: 0,
|
||||
}),
|
||||
).toEqual({
|
||||
path: "<root>",
|
||||
message: "invalid",
|
||||
allowedValues: ["stable", "beta"],
|
||||
});
|
||||
|
||||
it("normalizes issue collections for machine output", () => {
|
||||
expect(
|
||||
normalizeConfigIssues([
|
||||
{
|
||||
@@ -86,25 +64,6 @@ describe("config issue format", () => {
|
||||
allowedValuesHiddenCount: 2,
|
||||
},
|
||||
]),
|
||||
).toEqual([
|
||||
{
|
||||
path: "update.channel",
|
||||
message: "invalid",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
normalizeConfigIssue({
|
||||
path: "update.channel",
|
||||
message: "invalid",
|
||||
allowedValues: ["stable"],
|
||||
allowedValuesHiddenCount: 2,
|
||||
}),
|
||||
).toEqual({
|
||||
path: "update.channel",
|
||||
message: "invalid",
|
||||
allowedValues: ["stable"],
|
||||
allowedValuesHiddenCount: 2,
|
||||
});
|
||||
).toEqual([{ path: "update.channel", message: "invalid" }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ type ConfigIssueSummaryOptions = ConfigIssueFormatOptions & {
|
||||
};
|
||||
|
||||
/** Normalize missing or blank config issue paths to the root marker used in CLI output. */
|
||||
export function normalizeConfigIssuePath(path: string | null | undefined): string {
|
||||
function normalizeConfigIssuePath(path: string | null | undefined): string {
|
||||
if (typeof path !== "string") {
|
||||
return "<root>";
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export function normalizeConfigIssuePath(path: string | null | undefined): strin
|
||||
}
|
||||
|
||||
/** Return the public config issue shape with a normalized path and non-empty allowed values. */
|
||||
export function normalizeConfigIssue(issue: ConfigValidationIssue): ConfigValidationIssue {
|
||||
function normalizeConfigIssue(issue: ConfigValidationIssue): ConfigValidationIssue {
|
||||
const hasAllowedValues = Array.isArray(issue.allowedValues) && issue.allowedValues.length > 0;
|
||||
return {
|
||||
path: normalizeConfigIssuePath(issue.path),
|
||||
|
||||
@@ -63,11 +63,6 @@ function resolveMarkdownModeFromSection(
|
||||
return isMarkdownTableMode(sectionMode) ? sectionMode : undefined;
|
||||
}
|
||||
|
||||
export type {
|
||||
ResolveMarkdownTableMode,
|
||||
ResolveMarkdownTableModeParams,
|
||||
} from "./markdown-tables.types.js";
|
||||
|
||||
export function resolveMarkdownTableMode(
|
||||
params: ResolveMarkdownTableModeParams,
|
||||
): MarkdownTableMode {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Config paths with user-facing metadata for audio understanding settings. */
|
||||
export const MEDIA_AUDIO_FIELD_KEYS = [
|
||||
const MEDIA_AUDIO_FIELD_KEYS = [
|
||||
"tools.media.audio.enabled",
|
||||
"tools.media.audio.maxBytes",
|
||||
"tools.media.audio.maxChars",
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
resolveWriteEnvSnapshotForPath,
|
||||
} from "./io.write-prepare.js";
|
||||
import { ConfigMutationConflictError } from "./mutation-conflict.js";
|
||||
import type { ConfigMutationBase } from "./mutation-types.js";
|
||||
import { assertConfigWriteAllowedInCurrentMode } from "./nix-mode-write-guard.js";
|
||||
import { resolveConfigPath } from "./paths.js";
|
||||
import {
|
||||
@@ -64,9 +65,6 @@ import {
|
||||
import type { ConfigFileSnapshot, OpenClawConfig } from "./types.js";
|
||||
import { validateConfigObjectWithPlugins } from "./validation.js";
|
||||
|
||||
/** Selects whether a mutation starts from runtime or source config shape. */
|
||||
export type ConfigMutationBase = "runtime" | "source";
|
||||
|
||||
const CONFIG_MUTATION_LOCK_OPTIONS = {
|
||||
retries: {
|
||||
retries: 80,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Selects whether a mutation starts from runtime or source config shape. */
|
||||
export type ConfigMutationBase = "runtime" | "source";
|
||||
@@ -1,5 +1,5 @@
|
||||
// Normalizes config.patch replacePaths shared by Gateway and agent preflight checks.
|
||||
export function normalizeConfigPatchReplacePath(value: string): string {
|
||||
function normalizeConfigPatchReplacePath(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.endsWith("[]")) {
|
||||
return trimmed.slice(0, -2).replace(/\[\d+\](?=\.)/g, "[]");
|
||||
|
||||
@@ -36,10 +36,6 @@ import type {
|
||||
} from "./plugin-auto-enable.types.js";
|
||||
import { ensurePluginAllowlisted } from "./plugins-allowlist.js";
|
||||
import type { OpenClawConfig } from "./types.openclaw.js";
|
||||
export type {
|
||||
PluginAutoEnableCandidate,
|
||||
PluginAutoEnableResult,
|
||||
} from "./plugin-auto-enable.types.js";
|
||||
|
||||
const EMPTY_PLUGIN_MANIFEST_REGISTRY: PluginManifestRegistry = {
|
||||
plugins: [],
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { redactSnapshotTestHints as mainSchemaHints } from "../../test/helpers/config/redact-snapshot-test-hints.js";
|
||||
import type { ConfigUiHints } from "../shared/config-ui-hints-types.js";
|
||||
import {
|
||||
REDACTED_SENTINEL,
|
||||
redactConfigSnapshot,
|
||||
restoreRedactedValues as restoreRedactedValues_orig,
|
||||
} from "./redact-snapshot.js";
|
||||
import { makeSnapshot, restoreRedactedValues } from "./redact-snapshot.test-helpers.js";
|
||||
import type { ConfigUiHints } from "./schema.js";
|
||||
|
||||
describe("restoreRedactedValues", () => {
|
||||
it("restores redacted URL endpoint fields on round-trip", () => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Provides assertions for redacted snapshot tests.
|
||||
import { expect } from "vitest";
|
||||
import { restoreRedactedValues as restoreRedactedValues_orig } from "./redact-snapshot.js";
|
||||
import type { ConfigUiHints } from "./schema.js";
|
||||
import type { ConfigFileSnapshot } from "./types.openclaw.js";
|
||||
|
||||
type ConfigUiHints = NonNullable<Parameters<typeof restoreRedactedValues_orig>[2]>;
|
||||
|
||||
/** Complete snapshot shape used by redaction tests. */
|
||||
export type TestSnapshot<TConfig extends Record<string, unknown>> = ConfigFileSnapshot & {
|
||||
parsed: TConfig;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { expectDefined } from "@openclaw/normalization-core";
|
||||
import JSON5 from "json5";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { redactSnapshotTestHints as mainSchemaHints } from "../../test/helpers/config/redact-snapshot-test-hints.js";
|
||||
import type { ConfigUiHints } from "../shared/config-ui-hints-types.js";
|
||||
import { materializeRuntimeConfig } from "./materialize.js";
|
||||
import { REDACTED_SENTINEL, redactConfigSnapshot } from "./redact-snapshot.js";
|
||||
import {
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
restoreRedactedValues,
|
||||
type TestSnapshot,
|
||||
} from "./redact-snapshot.test-helpers.js";
|
||||
import { buildConfigSchema, type ConfigUiHints } from "./schema.js";
|
||||
import { buildConfigSchema } from "./schema.js";
|
||||
import type { ConfigFileSnapshot, OpenClawConfig } from "./types.openclaw.js";
|
||||
|
||||
function expectNestedPairValue(
|
||||
|
||||
@@ -2,49 +2,13 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
GROUP_POLICY_BLOCKED_LABEL,
|
||||
resetMissingProviderGroupPolicyFallbackWarningsForTesting,
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
resolveOpenProviderRuntimeGroupPolicy,
|
||||
resolveRuntimeGroupPolicy,
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
} from "./runtime-group-policy.js";
|
||||
|
||||
beforeEach(() => {
|
||||
resetMissingProviderGroupPolicyFallbackWarningsForTesting();
|
||||
});
|
||||
|
||||
describe("resolveRuntimeGroupPolicy", () => {
|
||||
it.each([
|
||||
{
|
||||
title: "fails closed when provider config is missing and no defaults are set",
|
||||
params: { providerConfigPresent: false },
|
||||
expectedPolicy: "allowlist",
|
||||
expectedFallbackApplied: true,
|
||||
},
|
||||
{
|
||||
title: "keeps configured fallback when provider config is present",
|
||||
params: { providerConfigPresent: true, configuredFallbackPolicy: "open" as const },
|
||||
expectedPolicy: "open",
|
||||
expectedFallbackApplied: false,
|
||||
},
|
||||
{
|
||||
title: "ignores global defaults when provider config is missing",
|
||||
params: {
|
||||
providerConfigPresent: false,
|
||||
defaultGroupPolicy: "disabled" as const,
|
||||
configuredFallbackPolicy: "open" as const,
|
||||
missingProviderFallbackPolicy: "allowlist" as const,
|
||||
},
|
||||
expectedPolicy: "allowlist",
|
||||
expectedFallbackApplied: true,
|
||||
},
|
||||
])("$title", ({ params, expectedPolicy, expectedFallbackApplied }) => {
|
||||
const resolved = resolveRuntimeGroupPolicy(params);
|
||||
expect(resolved.groupPolicy).toBe(expectedPolicy);
|
||||
expect(resolved.providerMissingFallbackApplied).toBe(expectedFallbackApplied);
|
||||
});
|
||||
});
|
||||
beforeEach(() => {});
|
||||
|
||||
describe("resolveOpenProviderRuntimeGroupPolicy", () => {
|
||||
it("uses open fallback when provider config exists", () => {
|
||||
|
||||
@@ -19,9 +19,7 @@ type RuntimeGroupPolicyParams = {
|
||||
* Resolve the effective group policy for a channel/provider runtime.
|
||||
* Missing provider config can fail closed separately from configured providers.
|
||||
*/
|
||||
export function resolveRuntimeGroupPolicy(
|
||||
params: RuntimeGroupPolicyParams,
|
||||
): RuntimeGroupPolicyResolution {
|
||||
function resolveRuntimeGroupPolicy(params: RuntimeGroupPolicyParams): RuntimeGroupPolicyResolution {
|
||||
const configuredFallbackPolicy = params.configuredFallbackPolicy ?? "open";
|
||||
const missingProviderFallbackPolicy = params.missingProviderFallbackPolicy ?? "allowlist";
|
||||
const groupPolicy = params.providerConfigPresent
|
||||
@@ -119,10 +117,3 @@ export function warnMissingProviderGroupPolicyFallbackOnce(params: {
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test helper. Keeps warning-cache state deterministic across test files.
|
||||
*/
|
||||
export function resetMissingProviderGroupPolicyFallbackWarningsForTesting(): void {
|
||||
warnedMissingProviderGroupPolicy.clear();
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ function applyNodeDocumentation(
|
||||
}
|
||||
}
|
||||
|
||||
export type BaseConfigSchemaResponse = {
|
||||
type BaseConfigSchemaResponse = {
|
||||
schema: ConfigSchema;
|
||||
uiHints: ConfigUiHints;
|
||||
version: string;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MEDIA_AUDIO_FIELD_KEYS } from "./media-audio-field-metadata.js";
|
||||
import { MEDIA_AUDIO_FIELD_HELP } from "./media-audio-field-metadata.js";
|
||||
import { FIELD_HELP } from "./schema.help.js";
|
||||
import { FIELD_LABELS } from "./schema.labels.js";
|
||||
|
||||
@@ -516,7 +516,7 @@ const TOOLS_HOOKS_TARGET_KEYS = [
|
||||
"tools.links.models",
|
||||
"tools.links.scope",
|
||||
"tools.links.timeoutSeconds",
|
||||
...MEDIA_AUDIO_FIELD_KEYS,
|
||||
...Object.keys(MEDIA_AUDIO_FIELD_HELP),
|
||||
"tools.media.concurrency",
|
||||
"tools.media.image.attachments",
|
||||
"tools.media.image.enabled",
|
||||
|
||||
@@ -3,9 +3,8 @@ import { isSensitiveUrlConfigPath } from "@openclaw/net-policy/redact-sensitive-
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { buildSecretInputSchema } from "../plugin-sdk/secret-input-schema.js";
|
||||
import { FIELD_HELP } from "./schema.help.js";
|
||||
import { testApi, isPluginOwnedChannelHintPath, isSensitiveConfigPath } from "./schema.hints.js";
|
||||
import { FIELD_LABELS } from "./schema.labels.js";
|
||||
import { buildBaseHints, testApi } from "./schema.hints.js";
|
||||
import { isSensitiveConfigPath } from "./sensitive-paths.js";
|
||||
import { OpenClawSchema } from "./zod-schema.js";
|
||||
import { sensitive } from "./zod-schema.sensitive.js";
|
||||
|
||||
@@ -23,7 +22,7 @@ const BUNDLED_CHANNEL_HINT_PREFIXES = [
|
||||
|
||||
describe("isSensitiveConfigPath", () => {
|
||||
it("matches whitelist suffixes case-insensitively", () => {
|
||||
const whitelistedPaths = [
|
||||
for (const path of [
|
||||
"maxTokens",
|
||||
"maxOutputTokens",
|
||||
"maxInputTokens",
|
||||
@@ -34,8 +33,7 @@ describe("isSensitiveConfigPath", () => {
|
||||
"tokenLimit",
|
||||
"tokenBudget",
|
||||
"channels.irc.nickserv.passwordFile",
|
||||
];
|
||||
for (const path of whitelistedPaths) {
|
||||
]) {
|
||||
expect(isSensitiveConfigPath(path)).toBe(false);
|
||||
expect(isSensitiveConfigPath(path.toUpperCase())).toBe(false);
|
||||
}
|
||||
@@ -46,25 +44,20 @@ describe("isSensitiveConfigPath", () => {
|
||||
expect(isSensitiveConfigPath("models.providers.openai.apiKey")).toBe(true);
|
||||
expect(isSensitiveConfigPath("channels.irc.nickserv.password")).toBe(true);
|
||||
expect(isSensitiveConfigPath("channels.feishu.encryptKey")).toBe(true);
|
||||
expect(isSensitiveConfigPath("channels.feishu.accounts.default.encryptKey")).toBe(true);
|
||||
expect(isSensitiveConfigPath("channels.nostr.privateKey")).toBe(true);
|
||||
expect(isSensitiveConfigPath("channels.nostr.accounts.default.privateKey")).toBe(true);
|
||||
expect(isSensitiveConfigPath("models.providers.local.localService.env.HF_HOME")).toBe(true);
|
||||
expect(isSensitiveConfigPath("models.providers.local.localService.env.MAX_TOKENS")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin-owned channel hint paths", () => {
|
||||
it("keeps bundled channel help and labels out of core tables", () => {
|
||||
for (const key of [...Object.keys(FIELD_HELP), ...Object.keys(FIELD_LABELS)]) {
|
||||
if (
|
||||
!BUNDLED_CHANNEL_HINT_PREFIXES.some(
|
||||
it("keeps bundled channel hints out of the core hint map", () => {
|
||||
for (const key of Object.keys(buildBaseHints())) {
|
||||
expect(
|
||||
BUNDLED_CHANNEL_HINT_PREFIXES.some(
|
||||
(prefix) => key === prefix || key.startsWith(`${prefix}.`),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
expect(isPluginOwnedChannelHintPath(key), `core still owns ${key}`).toBe(false);
|
||||
),
|
||||
`core still owns ${key}`,
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,15 +111,13 @@ function isKernelOwnedChannelHintPath(path: string): boolean {
|
||||
}
|
||||
|
||||
/** Return whether a channel hint path belongs to a plugin-owned channel namespace. */
|
||||
export function isPluginOwnedChannelHintPath(path: string): boolean {
|
||||
function isPluginOwnedChannelHintPath(path: string): boolean {
|
||||
if (!path.startsWith(CHANNEL_NAMESPACE_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
return !isKernelOwnedChannelHintPath(path);
|
||||
}
|
||||
|
||||
export { isSensitiveConfigPath };
|
||||
|
||||
/** Build core config UI hints while leaving plugin-owned channel hints to plugin schemas. */
|
||||
export function buildBaseHints(): ConfigUiHints {
|
||||
const hints: ConfigUiHints = {};
|
||||
@@ -330,4 +328,3 @@ export const testApi = {
|
||||
collectMatchingSchemaPaths,
|
||||
mapSensitivePaths,
|
||||
};
|
||||
export { testApi as __test__ };
|
||||
|
||||
@@ -3,7 +3,7 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/st
|
||||
import type { ConfigUiHint, ConfigUiHints } from "../shared/config-ui-hints-types.js";
|
||||
|
||||
/** Stable config UI tag vocabulary used for filtering and grouping schema hints. */
|
||||
export const CONFIG_TAGS = [
|
||||
const CONFIG_TAGS = [
|
||||
"security",
|
||||
"auth",
|
||||
"network",
|
||||
@@ -21,7 +21,7 @@ export const CONFIG_TAGS = [
|
||||
"advanced",
|
||||
] as const;
|
||||
|
||||
export type ConfigTag = (typeof CONFIG_TAGS)[number];
|
||||
type ConfigTag = (typeof CONFIG_TAGS)[number];
|
||||
|
||||
const TAG_PRIORITY: Record<ConfigTag, number> = {
|
||||
security: 0,
|
||||
@@ -162,7 +162,7 @@ function addTags(set: Set<ConfigTag>, tags: ReadonlyArray<ConfigTag>): void {
|
||||
}
|
||||
|
||||
/** Derive known config UI tags from a schema path and optional hint metadata. */
|
||||
export function deriveTagsForPath(path: string, hint?: ConfigUiHint): ConfigTag[] {
|
||||
function deriveTagsForPath(path: string, hint?: ConfigUiHint): ConfigTag[] {
|
||||
const lowerPath = normalizeLowercaseStringOrEmpty(path);
|
||||
const override = resolveOverride(path);
|
||||
if (override) {
|
||||
|
||||
+22
-45
@@ -3,7 +3,7 @@ import { SENSITIVE_URL_HINT_TAG } from "@openclaw/net-policy/redact-sensitive-ur
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { buildConfigSchema, lookupConfigSchema } from "./schema.js";
|
||||
import { applyDerivedTags, CONFIG_TAGS, deriveTagsForPath } from "./schema.tags.js";
|
||||
import { applyDerivedTags } from "./schema.tags.js";
|
||||
import { ToolsSchema } from "./zod-schema.agent-runtime.js";
|
||||
import { OpenClawSchema } from "./zod-schema.js";
|
||||
import {
|
||||
@@ -574,23 +574,29 @@ describe("config schema", () => {
|
||||
expect(second).toBe(first);
|
||||
});
|
||||
|
||||
it("derives security/auth tags for credential paths", () => {
|
||||
const tags = deriveTagsForPath("gateway.auth.token");
|
||||
expect(tags).toContain("security");
|
||||
expect(tags).toContain("auth");
|
||||
it("derives tags for security, network, storage, tools, and performance paths", () => {
|
||||
const tagged = applyDerivedTags({
|
||||
"gateway.auth.token": {},
|
||||
"proxy.tls.caFile": {},
|
||||
"tools.web.fetch.timeoutSeconds": {},
|
||||
});
|
||||
expect(tagged["gateway.auth.token"]?.tags).toEqual(
|
||||
expect.arrayContaining(["security", "auth"]),
|
||||
);
|
||||
expect(tagged["proxy.tls.caFile"]?.tags).toEqual(
|
||||
expect.arrayContaining(["security", "network", "storage"]),
|
||||
);
|
||||
expect(tagged["tools.web.fetch.timeoutSeconds"]?.tags).toEqual(
|
||||
expect.arrayContaining(["tools", "performance"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("classifies managed proxy CA files as security-relevant config", () => {
|
||||
const tags = deriveTagsForPath("proxy.tls.caFile");
|
||||
expect(tags).toContain("security");
|
||||
expect(tags).toContain("network");
|
||||
expect(tags).toContain("storage");
|
||||
});
|
||||
|
||||
it("derives tools/performance tags for web fetch timeout paths", () => {
|
||||
const tags = deriveTagsForPath("tools.web.fetch.timeoutSeconds");
|
||||
expect(tags).toContain("tools");
|
||||
expect(tags).toContain("performance");
|
||||
it("covers core config paths with derived tags", () => {
|
||||
for (const [key, hint] of Object.entries(baseSchema.uiHints)) {
|
||||
if (key.includes(".")) {
|
||||
expect(hint.tags?.length ?? 0, `expected tags for ${key}`).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts web fetch readability and firecrawl config in the runtime zod schema", () => {
|
||||
@@ -1013,35 +1019,6 @@ describe("config schema", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps tags in the allowed taxonomy", () => {
|
||||
const withTags = applyDerivedTags({
|
||||
"gateway.auth.token": {},
|
||||
"tools.web.fetch.timeoutSeconds": {},
|
||||
"channels.slack.accounts.*.token": {},
|
||||
});
|
||||
const allowed = new Set<string>(CONFIG_TAGS);
|
||||
for (const hint of Object.values(withTags)) {
|
||||
for (const tag of hint.tags ?? []) {
|
||||
expect(allowed.has(tag)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("covers core/built-in config paths with tags", () => {
|
||||
const schema = baseSchema;
|
||||
const allowed = new Set<string>([...CONFIG_TAGS, SENSITIVE_URL_HINT_TAG]);
|
||||
for (const [key, hint] of Object.entries(schema.uiHints)) {
|
||||
if (!key.includes(".")) {
|
||||
continue;
|
||||
}
|
||||
const tags = hint.tags ?? [];
|
||||
expect(tags.length, `expected tags for ${key}`).toBeGreaterThan(0);
|
||||
for (const tag of tags) {
|
||||
expect(allowed.has(tag), `unexpected tag ${tag} on ${key}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("looks up a config schema path with immediate child summaries", () => {
|
||||
const lookup = lookupConfigSchema(baseSchema, "gateway.auth");
|
||||
expect(lookup?.path).toBe("gateway.auth");
|
||||
|
||||
@@ -15,9 +15,7 @@ import {
|
||||
} from "./schema.shared.js";
|
||||
import { applyDerivedTags } from "./schema.tags.js";
|
||||
|
||||
export type { ConfigUiHint, ConfigUiHints } from "./schema.hints.js";
|
||||
|
||||
export type ConfigSchema = Record<string, unknown>;
|
||||
type ConfigSchema = Record<string, unknown>;
|
||||
|
||||
type JsonSchemaNode = Record<string, unknown>;
|
||||
|
||||
@@ -108,7 +106,7 @@ export type ConfigSchemaResponse = {
|
||||
generatedAt: string;
|
||||
};
|
||||
|
||||
export type ConfigSchemaLookupChild = {
|
||||
type ConfigSchemaLookupChild = {
|
||||
key: string;
|
||||
path: string;
|
||||
type?: string | string[];
|
||||
@@ -119,17 +117,17 @@ export type ConfigSchemaLookupChild = {
|
||||
hintPath?: string;
|
||||
};
|
||||
|
||||
export type ConfigSchemaReloadKind = "restart" | "hot" | "none";
|
||||
type ConfigSchemaReloadKind = "restart" | "hot" | "none";
|
||||
|
||||
export type ConfigSchemaReloadMetadata = {
|
||||
type ConfigSchemaReloadMetadata = {
|
||||
kind: ConfigSchemaReloadKind;
|
||||
};
|
||||
|
||||
export type ConfigSchemaReloadMetadataResolver = (
|
||||
type ConfigSchemaReloadMetadataResolver = (
|
||||
path: string,
|
||||
) => ConfigSchemaReloadMetadata | null | undefined;
|
||||
|
||||
export type ConfigSchemaLookupResult = {
|
||||
type ConfigSchemaLookupResult = {
|
||||
path: string;
|
||||
schema: JsonSchemaNode;
|
||||
reloadKind?: ConfigSchemaReloadKind;
|
||||
|
||||
@@ -7,11 +7,7 @@ import * as jsonFiles from "../infra/json-files.js";
|
||||
import { createCanonicalFixtureSkill } from "../skills/test-support/test-helpers.js";
|
||||
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js";
|
||||
import {
|
||||
getSerializedSessionStore,
|
||||
getSerializedSessionStoreCacheStatsForTest,
|
||||
getSerializedSessionStorePromptRefs,
|
||||
getSessionStoreSnapshotCacheStatsForTest,
|
||||
getSessionStoreStringInternStatsForTest,
|
||||
readSessionStoreCache,
|
||||
setSerializedSessionStore,
|
||||
setSerializedSessionStorePromptRefs,
|
||||
@@ -20,14 +16,9 @@ import {
|
||||
import {
|
||||
clearSessionStoreCacheForTest,
|
||||
loadSessionStore,
|
||||
readSessionEntries,
|
||||
readSessionEntry,
|
||||
readSessionStoreSnapshot,
|
||||
readSessionUpdatedAt,
|
||||
saveSessionStore,
|
||||
updateSessionStore,
|
||||
updateSessionStoreEntry,
|
||||
updateLastRoute,
|
||||
} from "./sessions/store.js";
|
||||
import type { SessionEntry } from "./sessions/types.js";
|
||||
import type { SessionSkillPromptRef } from "./sessions/types.js";
|
||||
@@ -78,34 +69,6 @@ describe("Session Store Cache", () => {
|
||||
delete process.env.OPENCLAW_SESSION_SERIALIZED_CACHE_MAX_BYTES;
|
||||
});
|
||||
|
||||
it("bounds the serialized session store cache by total bytes", () => {
|
||||
process.env.OPENCLAW_SESSION_SERIALIZED_CACHE_MAX_BYTES = "64";
|
||||
clearSessionStoreCacheForTest();
|
||||
|
||||
setSerializedSessionStore("store:1", "a".repeat(40));
|
||||
setSerializedSessionStore("store:2", "b".repeat(40));
|
||||
|
||||
expect(getSerializedSessionStore("store:1")).toBeUndefined();
|
||||
expect(getSerializedSessionStore("store:2")).toBe("b".repeat(40));
|
||||
expect(getSerializedSessionStoreCacheStatsForTest().entries).toBe(1);
|
||||
expect(getSerializedSessionStoreCacheStatsForTest().totalBytes).toBe(40);
|
||||
});
|
||||
|
||||
it("bounds the serialized session store cache by path count", () => {
|
||||
const maxEntries = getSerializedSessionStoreCacheStatsForTest().maxEntries;
|
||||
|
||||
for (let index = 0; index < maxEntries + 2; index += 1) {
|
||||
setSerializedSessionStore(`store:${index}`, `serialized:${index}`);
|
||||
}
|
||||
|
||||
expect(getSerializedSessionStore("store:0")).toBeUndefined();
|
||||
expect(getSerializedSessionStore("store:1")).toBeUndefined();
|
||||
expect(getSerializedSessionStore(`store:${maxEntries + 1}`)).toBe(
|
||||
`serialized:${maxEntries + 1}`,
|
||||
);
|
||||
expect(getSerializedSessionStoreCacheStatsForTest().entries).toBe(maxEntries);
|
||||
});
|
||||
|
||||
it("keeps serialized prompt refs on the serialized cache entry lifecycle", () => {
|
||||
const promptRef: SessionSkillPromptRef = {
|
||||
version: 1,
|
||||
@@ -461,270 +424,6 @@ describe("Session Store Cache", () => {
|
||||
stringifySpy.mockRestore();
|
||||
});
|
||||
|
||||
it("interns duplicate large skillsSnapshot prompts across cached loads", async () => {
|
||||
const largePrompt = "skill prompt ".repeat(200);
|
||||
const testStore = {
|
||||
"session:1": createSessionEntry({
|
||||
skillsSnapshot: {
|
||||
prompt: largePrompt,
|
||||
skills: [{ name: "alpha" }],
|
||||
},
|
||||
}),
|
||||
"session:2": createSessionEntry({
|
||||
sessionId: "id-2",
|
||||
displayName: "Test Session 2",
|
||||
skillsSnapshot: {
|
||||
prompt: largePrompt,
|
||||
skills: [{ name: "beta" }],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
await saveSessionStore(storePath, testStore);
|
||||
clearSessionStoreCacheForTest();
|
||||
|
||||
const loaded1 = loadSessionStore(storePath);
|
||||
const afterFirstLoad = getSessionStoreStringInternStatsForTest();
|
||||
expect(afterFirstLoad.poolSize).toBe(1);
|
||||
expect(afterFirstLoad.stored).toBe(1);
|
||||
expect(afterFirstLoad.reused).toBeGreaterThanOrEqual(1);
|
||||
|
||||
for (const skill of expectDefined(loaded1["session:1"], "loaded session").skillsSnapshot
|
||||
?.skills ?? []) {
|
||||
skill.name = "mutated";
|
||||
break;
|
||||
}
|
||||
|
||||
const loaded2 = loadSessionStore(storePath);
|
||||
const afterSecondLoad = getSessionStoreStringInternStatsForTest();
|
||||
expect(afterSecondLoad.poolSize).toBe(1);
|
||||
expect(afterSecondLoad.reused).toBeGreaterThanOrEqual(afterFirstLoad.reused + 2);
|
||||
expect(
|
||||
expectDefined(loaded2["session:1"], 'loaded2["session:1"] test invariant').skillsSnapshot
|
||||
?.skills?.[0]?.name,
|
||||
).toBe("alpha");
|
||||
});
|
||||
|
||||
it("does not intern short skillsSnapshot prompts", async () => {
|
||||
const testStore = {
|
||||
"session:1": createSessionEntry({
|
||||
skillsSnapshot: {
|
||||
prompt: "short prompt",
|
||||
skills: [{ name: "alpha" }],
|
||||
},
|
||||
}),
|
||||
"session:2": createSessionEntry({
|
||||
sessionId: "id-2",
|
||||
displayName: "Test Session 2",
|
||||
skillsSnapshot: {
|
||||
prompt: "short prompt",
|
||||
skills: [{ name: "beta" }],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
await saveSessionStore(storePath, testStore);
|
||||
clearSessionStoreCacheForTest();
|
||||
|
||||
loadSessionStore(storePath);
|
||||
|
||||
const stats = getSessionStoreStringInternStatsForTest();
|
||||
expect(stats.poolSize).toBe(0);
|
||||
expect(stats.skippedSmall).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("reads updatedAt from immutable session snapshots without cloning cached stores", async () => {
|
||||
const updatedAt = Date.now();
|
||||
const testStore = createSingleSessionStore(
|
||||
createSessionEntry({
|
||||
updatedAt,
|
||||
}),
|
||||
"agent:main:main",
|
||||
);
|
||||
|
||||
await saveSessionStore(storePath, testStore);
|
||||
clearSessionStoreCacheForTest();
|
||||
readSessionStoreSnapshot(storePath);
|
||||
expect(readSessionEntry(storePath, "agent:main:main")?.updatedAt).toBe(updatedAt);
|
||||
|
||||
const parseSpy = vi.spyOn(JSON, "parse");
|
||||
|
||||
expect(readSessionUpdatedAt({ storePath, sessionKey: "agent:main:main" })).toBe(updatedAt);
|
||||
expect(parseSpy).not.toHaveBeenCalled();
|
||||
expect(getSessionStoreSnapshotCacheStatsForTest().entries).toBe(1);
|
||||
|
||||
parseSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("builds a snapshot from disk without reparsing the mutable clone", async () => {
|
||||
const testStore = createSingleSessionStore(
|
||||
createSessionEntry({
|
||||
skillsSnapshot: {
|
||||
prompt: "snapshot skill prompt ".repeat(200),
|
||||
skills: [{ name: "alpha" }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await saveSessionStore(storePath, testStore);
|
||||
clearSessionStoreCacheForTest();
|
||||
|
||||
const parseSpy = vi.spyOn(JSON, "parse");
|
||||
|
||||
const snapshot = readSessionStoreSnapshot(storePath);
|
||||
|
||||
expect(
|
||||
expectDefined(snapshot["session:1"], 'snapshot["session:1"] test invariant').sessionId,
|
||||
).toBe("id-1");
|
||||
expect(parseSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
parseSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("serves immutable session snapshots without cloning cache hits", async () => {
|
||||
const testStore = createSingleSessionStore(
|
||||
createSessionEntry({
|
||||
origin: { provider: "openai" },
|
||||
skillsSnapshot: {
|
||||
prompt: "snapshot skill prompt ".repeat(200),
|
||||
skills: [{ name: "alpha" }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await saveSessionStore(storePath, testStore);
|
||||
clearSessionStoreCacheForTest();
|
||||
|
||||
const snapshot1 = readSessionStoreSnapshot(storePath);
|
||||
const snapshot2 = readSessionStoreSnapshot(storePath);
|
||||
|
||||
expect(snapshot2).toBe(snapshot1);
|
||||
expect(Object.isFrozen(snapshot1)).toBe(true);
|
||||
expect(Object.isFrozen(snapshot1["session:1"])).toBe(true);
|
||||
expect(
|
||||
Object.isFrozen(
|
||||
expectDefined(snapshot1["session:1"], 'snapshot1["session:1"] test invariant')
|
||||
.skillsSnapshot?.skills,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(readSessionEntry(storePath, "session:1")?.sessionId).toBe("id-1");
|
||||
expect(readSessionEntries(storePath).map(([key]) => key)).toEqual(["session:1"]);
|
||||
|
||||
expect(() => {
|
||||
(snapshot1 as Record<string, SessionEntry>)["session:2"] = createSessionEntry({
|
||||
sessionId: "id-2",
|
||||
});
|
||||
}).toThrow(TypeError);
|
||||
|
||||
const mutable = loadSessionStore(storePath);
|
||||
expectDefined(mutable["session:1"], 'mutable["session:1"] test invariant').origin = {
|
||||
provider: "mutated",
|
||||
};
|
||||
|
||||
expect(
|
||||
expectDefined(
|
||||
readSessionStoreSnapshot(storePath)["session:1"],
|
||||
'readSessionStoreSnapshot(storePath)["session:1"] test invariant',
|
||||
).origin?.provider,
|
||||
).toBe("openai");
|
||||
});
|
||||
|
||||
it("reads immutable single entries without populating whole-store snapshots", async () => {
|
||||
await saveSessionStore(storePath, {
|
||||
"session:1": createSessionEntry({
|
||||
sessionId: "id-1",
|
||||
skillsSnapshot: {
|
||||
prompt: "single entry prompt ".repeat(200),
|
||||
skills: [{ name: "alpha" }],
|
||||
},
|
||||
}),
|
||||
"session:2": createSessionEntry({ sessionId: "id-2" }),
|
||||
});
|
||||
clearSessionStoreCacheForTest();
|
||||
|
||||
const entry = readSessionEntry(storePath, "session:1");
|
||||
|
||||
expect(entry?.sessionId).toBe("id-1");
|
||||
expect(Object.isFrozen(entry)).toBe(true);
|
||||
expect(Object.isFrozen(entry?.skillsSnapshot?.skills)).toBe(true);
|
||||
expect(getSessionStoreSnapshotCacheStatsForTest().entries).toBe(0);
|
||||
|
||||
const cached = loadSessionStore(storePath, { clone: false });
|
||||
expect(() => {
|
||||
(entry as SessionEntry).displayName = "mutated returned entry";
|
||||
}).toThrow(TypeError);
|
||||
expect(
|
||||
expectDefined(cached["session:1"], 'cached["session:1"] test invariant').displayName,
|
||||
).toBe("Test Session 1");
|
||||
});
|
||||
|
||||
it("does not tag snapshots with stats from writes racing after a disk read", async () => {
|
||||
await saveSessionStore(
|
||||
storePath,
|
||||
createSingleSessionStore(createSessionEntry({ displayName: "Before race" })),
|
||||
);
|
||||
clearSessionStoreCacheForTest();
|
||||
|
||||
const afterRaceStore = createSingleSessionStore(
|
||||
createSessionEntry({ displayName: "After cross-process race" }),
|
||||
);
|
||||
const originalReadFileSync = fs.readFileSync.bind(fs);
|
||||
let wroteAfterRead = false;
|
||||
const readSpy = vi.spyOn(fs, "readFileSync").mockImplementation((file, ...args) => {
|
||||
const result = originalReadFileSync(
|
||||
file,
|
||||
...(args as [Parameters<typeof fs.readFileSync>[1]]),
|
||||
);
|
||||
if (file === storePath && !wroteAfterRead) {
|
||||
wroteAfterRead = true;
|
||||
fs.writeFileSync(storePath, JSON.stringify(afterRaceStore, null, 2));
|
||||
const bumped = new Date(Date.now() + 2_000);
|
||||
fs.utimesSync(storePath, bumped, bumped);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const first = readSessionStoreSnapshot(storePath);
|
||||
expect(expectDefined(first["session:1"], 'first["session:1"] test invariant').displayName).toBe(
|
||||
"Before race",
|
||||
);
|
||||
|
||||
readSpy.mockRestore();
|
||||
|
||||
const second = readSessionStoreSnapshot(storePath);
|
||||
expect(
|
||||
expectDefined(second["session:1"], 'second["session:1"] test invariant').displayName,
|
||||
).toBe("After cross-process race");
|
||||
});
|
||||
|
||||
it("publishes a new immutable snapshot after session store writes", async () => {
|
||||
await saveSessionStore(storePath, createSingleSessionStore());
|
||||
|
||||
const before = readSessionStoreSnapshot(storePath);
|
||||
|
||||
await updateSessionStore(
|
||||
storePath,
|
||||
(store) => {
|
||||
store["session:1"] = {
|
||||
...expectDefined(store["session:1"], "stored session entry"),
|
||||
displayName: "Updated Session",
|
||||
updatedAt: Date.now() + 1,
|
||||
};
|
||||
},
|
||||
{ skipMaintenance: true },
|
||||
);
|
||||
|
||||
const after = readSessionStoreSnapshot(storePath);
|
||||
|
||||
expect(after).not.toBe(before);
|
||||
expect(
|
||||
expectDefined(before["session:1"], 'before["session:1"] test invariant').displayName,
|
||||
).toBe("Test Session 1");
|
||||
expect(expectDefined(after["session:1"], 'after["session:1"] test invariant').displayName).toBe(
|
||||
"Updated Session",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps whole-store update results detached from the mutable cache by default", async () => {
|
||||
await saveSessionStore(storePath, createSingleSessionStore());
|
||||
|
||||
@@ -821,30 +520,28 @@ describe("Session Store Cache", () => {
|
||||
).toBe("Entry writer owned by default");
|
||||
});
|
||||
|
||||
it("publishes route updates without cloning the whole object cache", async () => {
|
||||
it("detaches caller-owned patch objects before publishing writer-owned caches", async () => {
|
||||
await saveSessionStore(storePath, {
|
||||
"session:1": createSessionEntry({ sessionId: "id-1" }),
|
||||
"session:2": createSessionEntry({ sessionId: "id-2" }),
|
||||
});
|
||||
const before = loadSessionStore(storePath, { clone: false });
|
||||
const untouched = before["session:2"];
|
||||
const deliveryContext = { channel: "telegram", to: "chat-1" };
|
||||
|
||||
const persisted = await updateLastRoute({
|
||||
await updateSessionStoreEntry({
|
||||
storePath,
|
||||
sessionKey: "session:1",
|
||||
channel: "telegram",
|
||||
to: "chat-1",
|
||||
update: async () => ({ deliveryContext }),
|
||||
});
|
||||
deliveryContext.to = "mutated-after-persist";
|
||||
|
||||
const cached = loadSessionStore(storePath, { clone: false });
|
||||
expect(cached["session:2"]).toBe(untouched);
|
||||
expect(cached["session:1"]).not.toBe(persisted);
|
||||
persisted!.lastTo = "mutated-return";
|
||||
expect(expectDefined(cached["session:1"], 'cached["session:1"] test invariant').lastTo).toBe(
|
||||
"chat-1",
|
||||
);
|
||||
expect(
|
||||
expectDefined(cached["session:1"], 'cached["session:1"] test invariant').deliveryContext?.to,
|
||||
).toBe("chat-1");
|
||||
});
|
||||
|
||||
it("falls back to full projection when untouched entries need prompt blob repair", async () => {
|
||||
const prompt = "skill prompt ".repeat(80);
|
||||
await saveSessionStore(storePath, {
|
||||
@@ -944,40 +641,6 @@ describe("Session Store Cache", () => {
|
||||
expect(after).toBe(before);
|
||||
});
|
||||
|
||||
it("builds immutable session snapshots lazily after writes", async () => {
|
||||
await saveSessionStore(storePath, createSingleSessionStore());
|
||||
|
||||
expect(getSessionStoreSnapshotCacheStatsForTest().entries).toBe(0);
|
||||
|
||||
const first = readSessionStoreSnapshot(storePath);
|
||||
const statsAfterRead = getSessionStoreSnapshotCacheStatsForTest();
|
||||
const second = readSessionStoreSnapshot(storePath);
|
||||
|
||||
expect(first).toBe(second);
|
||||
expect(Object.isFrozen(first)).toBe(true);
|
||||
expect(statsAfterRead.entries).toBe(1);
|
||||
|
||||
await updateSessionStore(
|
||||
storePath,
|
||||
(store) => {
|
||||
store["session:1"] = {
|
||||
...expectDefined(store["session:1"], "stored session entry"),
|
||||
displayName: "Updated lazily",
|
||||
updatedAt: Date.now() + 1,
|
||||
};
|
||||
},
|
||||
{ skipMaintenance: true },
|
||||
);
|
||||
|
||||
expect(getSessionStoreSnapshotCacheStatsForTest().entries).toBe(0);
|
||||
expect(
|
||||
expectDefined(
|
||||
readSessionStoreSnapshot(storePath)["session:1"],
|
||||
'readSessionStoreSnapshot(storePath)["session:1"] test invariant',
|
||||
).displayName,
|
||||
).toBe("Updated lazily");
|
||||
});
|
||||
|
||||
it("should refresh cache when store file changes on disk", async () => {
|
||||
const testStore = createSingleSessionStore();
|
||||
|
||||
|
||||
@@ -10,17 +10,13 @@ import {
|
||||
buildGroupDisplayName,
|
||||
deriveSessionKey,
|
||||
loadSessionStore,
|
||||
patchSessionEntry,
|
||||
recordSessionMetaFromInbound,
|
||||
resolveSessionFilePath,
|
||||
resolveSessionFilePathOptions,
|
||||
resolveSessionKey,
|
||||
resolveSessionTranscriptPath,
|
||||
resolveSessionTranscriptsDir,
|
||||
updateLastRoute,
|
||||
updateSessionStore,
|
||||
updateSessionStoreEntry,
|
||||
upsertSessionEntry,
|
||||
} from "./sessions.js";
|
||||
|
||||
describe("sessions", () => {
|
||||
@@ -64,14 +60,6 @@ describe("sessions", () => {
|
||||
);
|
||||
}
|
||||
|
||||
function buildMainSessionEntry(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
sessionId: "sess-1",
|
||||
updatedAt: 123,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function createAgentSessionsLayout(label: string): Promise<{
|
||||
stateDir: string;
|
||||
mainStorePath: string;
|
||||
@@ -230,288 +218,6 @@ describe("sessions", () => {
|
||||
});
|
||||
}
|
||||
|
||||
it("updateLastRoute persists channel and target", async () => {
|
||||
const mainSessionKey = "agent:main:main";
|
||||
const { storePath } = await createSessionStoreFixture({
|
||||
prefix: "updateLastRoute",
|
||||
entries: {
|
||||
[mainSessionKey]: buildMainSessionEntry({
|
||||
systemSent: true,
|
||||
thinkingLevel: "low",
|
||||
responseUsage: "on",
|
||||
queueDebounceMs: 1234,
|
||||
reasoningLevel: "on",
|
||||
elevatedLevel: "on",
|
||||
authProfileOverride: "auth-1",
|
||||
compactionCount: 2,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await updateLastRoute({
|
||||
storePath,
|
||||
sessionKey: mainSessionKey,
|
||||
deliveryContext: {
|
||||
channel: "telegram",
|
||||
to: " 12345 ",
|
||||
},
|
||||
});
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
expect(store[mainSessionKey]?.sessionId).toBe("sess-1");
|
||||
// updateLastRoute must preserve existing updatedAt (activity timestamp)
|
||||
expect(store[mainSessionKey]?.updatedAt).toBe(123);
|
||||
expect(store[mainSessionKey]?.lastChannel).toBe("telegram");
|
||||
expect(store[mainSessionKey]?.lastTo).toBe("12345");
|
||||
expect(store[mainSessionKey]?.deliveryContext).toEqual({
|
||||
channel: "telegram",
|
||||
to: "12345",
|
||||
});
|
||||
expect(store[mainSessionKey]?.responseUsage).toBe("on");
|
||||
expect(store[mainSessionKey]?.queueDebounceMs).toBe(1234);
|
||||
expect(store[mainSessionKey]?.reasoningLevel).toBe("on");
|
||||
expect(store[mainSessionKey]?.elevatedLevel).toBe("on");
|
||||
expect(store[mainSessionKey]?.authProfileOverride).toBe("auth-1");
|
||||
expect(store[mainSessionKey]?.compactionCount).toBe(2);
|
||||
});
|
||||
|
||||
it("updateLastRoute prefers explicit deliveryContext", async () => {
|
||||
const mainSessionKey = "agent:main:main";
|
||||
const { storePath } = await createSessionStoreFixture({
|
||||
prefix: "updateLastRoute",
|
||||
entries: {},
|
||||
});
|
||||
|
||||
await updateLastRoute({
|
||||
storePath,
|
||||
sessionKey: mainSessionKey,
|
||||
channel: "demo-chat",
|
||||
to: "111",
|
||||
accountId: "legacy",
|
||||
deliveryContext: {
|
||||
channel: "telegram",
|
||||
to: "222",
|
||||
accountId: "primary",
|
||||
},
|
||||
});
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
expect(store[mainSessionKey]?.lastChannel).toBe("telegram");
|
||||
expect(store[mainSessionKey]?.lastTo).toBe("222");
|
||||
expect(store[mainSessionKey]?.lastAccountId).toBe("primary");
|
||||
expect(store[mainSessionKey]?.deliveryContext).toEqual({
|
||||
channel: "telegram",
|
||||
to: "222",
|
||||
accountId: "primary",
|
||||
});
|
||||
});
|
||||
|
||||
it("updateLastRoute clears threadId when explicit route omits threadId", async () => {
|
||||
const mainSessionKey = "agent:main:main";
|
||||
const { storePath } = await createSessionStoreFixture({
|
||||
prefix: "updateLastRoute",
|
||||
entries: {
|
||||
[mainSessionKey]: buildMainSessionEntry({
|
||||
deliveryContext: {
|
||||
channel: "telegram",
|
||||
to: "222",
|
||||
threadId: "42",
|
||||
},
|
||||
lastChannel: "telegram",
|
||||
lastTo: "222",
|
||||
lastThreadId: "42",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await updateLastRoute({
|
||||
storePath,
|
||||
sessionKey: mainSessionKey,
|
||||
deliveryContext: {
|
||||
channel: "telegram",
|
||||
to: "222",
|
||||
},
|
||||
});
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
expect(store[mainSessionKey]?.deliveryContext).toEqual({
|
||||
channel: "telegram",
|
||||
to: "222",
|
||||
});
|
||||
expect(store[mainSessionKey]?.lastThreadId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updateLastRoute records origin + group metadata when ctx is provided", async () => {
|
||||
const sessionKey = "agent:main:demo-chat:group:room-123";
|
||||
const { storePath } = await createSessionStoreFixture({
|
||||
prefix: "updateLastRoute",
|
||||
entries: {},
|
||||
});
|
||||
|
||||
await updateLastRoute({
|
||||
storePath,
|
||||
sessionKey,
|
||||
deliveryContext: {
|
||||
channel: "demo-chat",
|
||||
to: "room-123",
|
||||
},
|
||||
ctx: {
|
||||
Provider: "demo-chat",
|
||||
ChatType: "group",
|
||||
GroupSubject: "Family",
|
||||
From: "room-123",
|
||||
},
|
||||
});
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
expect(store[sessionKey]?.subject).toBe("Family");
|
||||
expect(store[sessionKey]?.channel).toBe("demo-chat");
|
||||
expect(store[sessionKey]?.groupId).toBe("room-123");
|
||||
expect(store[sessionKey]?.origin?.label).toBe("Family");
|
||||
expect(store[sessionKey]?.origin?.provider).toBe("demo-chat");
|
||||
expect(store[sessionKey]?.origin?.chatType).toBe("group");
|
||||
});
|
||||
|
||||
it("updateLastRoute skips missing sessions when creation is disabled", async () => {
|
||||
const sessionKey = "agent:main:demo-chat:group:room-123";
|
||||
const { storePath } = await createSessionStoreFixture({
|
||||
prefix: "updateLastRoute-no-create",
|
||||
entries: {},
|
||||
});
|
||||
|
||||
const result = await updateLastRoute({
|
||||
storePath,
|
||||
sessionKey,
|
||||
deliveryContext: {
|
||||
channel: "demo-chat",
|
||||
to: "room-123",
|
||||
},
|
||||
createIfMissing: false,
|
||||
});
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
expect(result).toBeNull();
|
||||
expect(store[sessionKey]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updateLastRoute updates existing sessions when creation is disabled", async () => {
|
||||
const sessionKey = "agent:main:demo-chat:group:room-123";
|
||||
const { storePath } = await createSessionStoreFixture({
|
||||
prefix: "updateLastRoute-existing-no-create",
|
||||
entries: {
|
||||
[sessionKey]: buildMainSessionEntry(),
|
||||
},
|
||||
});
|
||||
|
||||
await updateLastRoute({
|
||||
storePath,
|
||||
sessionKey,
|
||||
deliveryContext: {
|
||||
channel: "demo-chat",
|
||||
to: "room-123",
|
||||
},
|
||||
createIfMissing: false,
|
||||
});
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
expect(store[sessionKey]?.lastChannel).toBe("demo-chat");
|
||||
expect(store[sessionKey]?.lastTo).toBe("room-123");
|
||||
});
|
||||
|
||||
it("updateLastRoute does not bump updatedAt on existing sessions (#49515)", async () => {
|
||||
const mainSessionKey = "agent:main:main";
|
||||
const frozenUpdatedAt = 1000;
|
||||
const { storePath } = await createSessionStoreFixture({
|
||||
prefix: "updateLastRoute-preserve-activity",
|
||||
entries: {
|
||||
[mainSessionKey]: buildMainSessionEntry({
|
||||
updatedAt: frozenUpdatedAt,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await updateLastRoute({
|
||||
storePath,
|
||||
sessionKey: mainSessionKey,
|
||||
deliveryContext: {
|
||||
channel: "telegram",
|
||||
to: "99999",
|
||||
},
|
||||
});
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
// Route updates must not refresh activity timestamps; idle/daily reset
|
||||
// evaluation relies on updatedAt from actual session turns.
|
||||
expect(store[mainSessionKey]?.updatedAt).toBe(frozenUpdatedAt);
|
||||
// Routing fields should still be updated
|
||||
expect(store[mainSessionKey]?.lastChannel).toBe("telegram");
|
||||
expect(store[mainSessionKey]?.lastTo).toBe("99999");
|
||||
});
|
||||
|
||||
it("updateLastRoute skips persistence when the route is unchanged", async () => {
|
||||
const mainSessionKey = "agent:main:main";
|
||||
const entry = buildMainSessionEntry({
|
||||
route: {
|
||||
channel: "telegram",
|
||||
target: { to: "99999" },
|
||||
},
|
||||
deliveryContext: {
|
||||
channel: "telegram",
|
||||
to: "99999",
|
||||
},
|
||||
lastChannel: "telegram",
|
||||
lastTo: "99999",
|
||||
});
|
||||
const { storePath } = await createSessionStoreFixture({
|
||||
prefix: "updateLastRoute-noop",
|
||||
entries: {
|
||||
[mainSessionKey]: entry,
|
||||
},
|
||||
});
|
||||
const before = await fs.readFile(storePath, "utf-8");
|
||||
|
||||
const result = await updateLastRoute({
|
||||
storePath,
|
||||
sessionKey: mainSessionKey,
|
||||
deliveryContext: {
|
||||
channel: "telegram",
|
||||
to: "99999",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(entry);
|
||||
if (result) {
|
||||
result.lastTo = "mutated";
|
||||
}
|
||||
expect(loadSessionStore(storePath, { clone: false })[mainSessionKey]?.lastTo).toBe("99999");
|
||||
await expect(fs.readFile(storePath, "utf-8")).resolves.toBe(before);
|
||||
});
|
||||
|
||||
it("recordSessionMetaFromInbound skips persistence when there is no metadata patch", async () => {
|
||||
const mainSessionKey = "agent:main:main";
|
||||
const entry = buildMainSessionEntry();
|
||||
const { storePath } = await createSessionStoreFixture({
|
||||
prefix: "recordSessionMetaFromInbound-noop",
|
||||
entries: {
|
||||
[mainSessionKey]: entry,
|
||||
},
|
||||
});
|
||||
const before = await fs.readFile(storePath, "utf-8");
|
||||
|
||||
const result = await recordSessionMetaFromInbound({
|
||||
storePath,
|
||||
sessionKey: mainSessionKey,
|
||||
ctx: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual(entry);
|
||||
if (result) {
|
||||
result.sessionId = "mutated";
|
||||
}
|
||||
expect(loadSessionStore(storePath, { clone: false })[mainSessionKey]?.sessionId).toBe("sess-1");
|
||||
await expect(fs.readFile(storePath, "utf-8")).resolves.toBe(before);
|
||||
});
|
||||
|
||||
it("updateSessionStoreEntry preserves existing fields when patching", async () => {
|
||||
const sessionKey = "agent:main:main";
|
||||
const { storePath } = await createSessionStoreFixture({
|
||||
@@ -600,102 +306,6 @@ describe("sessions", () => {
|
||||
expect(loadSessionStore(storePath)[sessionKey]?.displayName).toBe("after");
|
||||
});
|
||||
|
||||
it("patchSessionEntry can preserve activity for metadata-only updates", async () => {
|
||||
const sessionKey = "agent:main:main";
|
||||
const { storePath } = await createSessionStoreFixture({
|
||||
prefix: "patchSessionEntry-preserve-activity",
|
||||
entries: {
|
||||
[sessionKey]: {
|
||||
sessionId: "sess-1",
|
||||
updatedAt: 100,
|
||||
pluginDebugEntries: [{ pluginId: "other", lines: ["keep"] }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await patchSessionEntry({
|
||||
storePath,
|
||||
sessionKey,
|
||||
preserveActivity: true,
|
||||
update: () => ({
|
||||
pluginDebugEntries: [{ pluginId: "active-memory", lines: ["status"] }],
|
||||
}),
|
||||
});
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
expect(store[sessionKey]?.updatedAt).toBe(100);
|
||||
expect(store[sessionKey]?.pluginDebugEntries).toEqual([
|
||||
{ pluginId: "active-memory", lines: ["status"] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("patchSessionEntry can replace an entry so deleted fields stay deleted", async () => {
|
||||
const sessionKey = "agent:main:main";
|
||||
const { storePath } = await createSessionStoreFixture({
|
||||
prefix: "patchSessionEntry-replace-entry",
|
||||
entries: {
|
||||
[sessionKey]: {
|
||||
sessionId: "sess-1",
|
||||
updatedAt: 100,
|
||||
model: "old-model",
|
||||
modelProvider: "old-provider",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await patchSessionEntry({
|
||||
storePath,
|
||||
sessionKey,
|
||||
replaceEntry: true,
|
||||
update: (entry) => {
|
||||
const next = { ...entry, providerOverride: "openai" };
|
||||
delete next.model;
|
||||
delete next.modelProvider;
|
||||
return next;
|
||||
},
|
||||
});
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
expect(store[sessionKey]?.providerOverride).toBe("openai");
|
||||
expect(store[sessionKey]?.model).toBeUndefined();
|
||||
expect(store[sessionKey]?.modelProvider).toBeUndefined();
|
||||
});
|
||||
|
||||
it("upsertSessionEntry drops legacy embedded ACP metadata", async () => {
|
||||
const sessionKey = "agent:main:main";
|
||||
const acp = {
|
||||
backend: "codex",
|
||||
agent: "main",
|
||||
runtimeSessionName: "runtime-session",
|
||||
mode: "persistent" as const,
|
||||
state: "idle" as const,
|
||||
lastActivityAt: 100,
|
||||
};
|
||||
const { storePath } = await createSessionStoreFixture({
|
||||
prefix: "upsertSessionEntry-acp",
|
||||
entries: {
|
||||
[sessionKey]: {
|
||||
sessionId: "sess-1",
|
||||
updatedAt: 100,
|
||||
acp,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await upsertSessionEntry({
|
||||
storePath,
|
||||
sessionKey,
|
||||
entry: {
|
||||
sessionId: "sess-2",
|
||||
updatedAt: 200,
|
||||
},
|
||||
});
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
expect(store[sessionKey]?.sessionId).toBe("sess-2");
|
||||
expect(store[sessionKey]?.acp).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updateSessionStore preserves concurrent additions", async () => {
|
||||
const dir = await createCaseDir("updateSessionStore");
|
||||
const storePath = path.join(dir, "sessions.json");
|
||||
|
||||
@@ -16,15 +16,6 @@ export {
|
||||
patchSessionEntryWithKey,
|
||||
resetSessionEntryLifecycle,
|
||||
resolveSessionEntryCandidateTarget,
|
||||
type CanonicalizeSessionEntryAliasesResult,
|
||||
type DeleteSessionEntryLifecycleParams,
|
||||
type DeleteSessionEntryLifecycleResult,
|
||||
type ResolvedSessionEntryCandidateTarget,
|
||||
type ResetSessionEntryLifecycleParams,
|
||||
type ResetSessionEntryLifecycleResult,
|
||||
type SessionEntryCandidateAccessScope,
|
||||
type SessionLifecycleArchivedTranscript,
|
||||
type SessionLifecycleStoreTarget,
|
||||
} from "./sessions/session-accessor.js";
|
||||
export * from "./sessions/session-key.js";
|
||||
export * from "./sessions/store.js";
|
||||
|
||||
@@ -6,11 +6,8 @@ import {
|
||||
isPrimarySessionTranscriptFileName,
|
||||
isSessionArchiveArtifactName,
|
||||
isSessionStoreTempArtifactName,
|
||||
isTrajectoryPointerArtifactName,
|
||||
isTrajectoryRuntimeArtifactName,
|
||||
isTrajectorySessionArtifactName,
|
||||
isUsageCountedSessionTranscriptFileName,
|
||||
parseCompactionCheckpointTranscriptFileName,
|
||||
parseUsageCountedSessionIdFromFileName,
|
||||
parseSessionArchiveTimestamp,
|
||||
} from "./artifacts.js";
|
||||
@@ -65,13 +62,20 @@ describe("session artifact helpers", () => {
|
||||
});
|
||||
|
||||
it("classifies trajectory sidecar artifacts", () => {
|
||||
expect(isTrajectoryRuntimeArtifactName("abc.trajectory.jsonl")).toBe(true);
|
||||
expect(isTrajectoryPointerArtifactName("abc.trajectory-path.json")).toBe(true);
|
||||
expect(isTrajectorySessionArtifactName("abc.trajectory.jsonl")).toBe(true);
|
||||
expect(isTrajectorySessionArtifactName("abc.trajectory-path.json")).toBe(true);
|
||||
expect(isTrajectorySessionArtifactName("abc.jsonl")).toBe(false);
|
||||
});
|
||||
|
||||
it("recognizes exact compaction checkpoint transcript names", () => {
|
||||
expect(
|
||||
isCompactionCheckpointTranscriptFileName(
|
||||
"abc.checkpoint.11111111-1111-4111-8111-111111111111.jsonl",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isCompactionCheckpointTranscriptFileName("abc.checkpoint.not-a-uuid.jsonl")).toBe(false);
|
||||
});
|
||||
|
||||
it("classifies usage-counted transcript files", () => {
|
||||
expect(isUsageCountedSessionTranscriptFileName("abc.jsonl")).toBe(true);
|
||||
expect(
|
||||
@@ -110,23 +114,6 @@ describe("session artifact helpers", () => {
|
||||
expect(parseUsageCountedSessionIdFromFileName("abc.trajectory.jsonl")).toBeNull();
|
||||
});
|
||||
|
||||
it("parses exact compaction checkpoint transcript file names", () => {
|
||||
expect(
|
||||
parseCompactionCheckpointTranscriptFileName(
|
||||
"abc.checkpoint.11111111-1111-4111-8111-111111111111.jsonl",
|
||||
),
|
||||
).toEqual({
|
||||
sessionId: "abc",
|
||||
checkpointId: "11111111-1111-4111-8111-111111111111",
|
||||
});
|
||||
expect(isCompactionCheckpointTranscriptFileName("abc.checkpoint.not-a-uuid.jsonl")).toBe(false);
|
||||
expect(
|
||||
isCompactionCheckpointTranscriptFileName(
|
||||
"abc.checkpoint.11111111-1111-4111-8111-111111111111.jsonl.deleted.2026-01-01T00-00-00.000Z",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("formats and parses archive timestamps", () => {
|
||||
const now = Date.parse("2026-02-23T12:34:56.000Z");
|
||||
const stamp = formatSessionArchiveTimestamp(now);
|
||||
|
||||
@@ -72,7 +72,7 @@ export function isSessionStoreTempArtifactName(fileName: string, storeBasename:
|
||||
}
|
||||
|
||||
/** Parses a compaction checkpoint transcript filename into session/checkpoint ids. */
|
||||
export function parseCompactionCheckpointTranscriptFileName(fileName: string): {
|
||||
function parseCompactionCheckpointTranscriptFileName(fileName: string): {
|
||||
sessionId: string;
|
||||
checkpointId: string;
|
||||
} | null {
|
||||
@@ -88,12 +88,12 @@ export function isCompactionCheckpointTranscriptFileName(fileName: string): bool
|
||||
}
|
||||
|
||||
/** Returns true for trajectory runtime jsonl artifacts. */
|
||||
export function isTrajectoryRuntimeArtifactName(fileName: string): boolean {
|
||||
function isTrajectoryRuntimeArtifactName(fileName: string): boolean {
|
||||
return fileName.endsWith(".trajectory.jsonl");
|
||||
}
|
||||
|
||||
/** Returns true for trajectory pointer artifacts. */
|
||||
export function isTrajectoryPointerArtifactName(fileName: string): boolean {
|
||||
function isTrajectoryPointerArtifactName(fileName: string): boolean {
|
||||
return fileName.endsWith(".trajectory-path.json");
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ export type SessionsCleanupOptions = SessionStoreSelectionOptions & {
|
||||
fixDmScope?: boolean;
|
||||
};
|
||||
|
||||
export type SessionCleanupAction =
|
||||
type SessionCleanupAction =
|
||||
| "keep"
|
||||
| "prune-missing"
|
||||
| "prune-model-run"
|
||||
@@ -88,7 +88,7 @@ export type SessionsCleanupResult =
|
||||
stores: SessionCleanupSummary[];
|
||||
};
|
||||
|
||||
export type SessionsCleanupRunResult = {
|
||||
type SessionsCleanupRunResult = {
|
||||
mode: ResolvedSessionMaintenanceConfig["mode"];
|
||||
previewResults: Array<{
|
||||
summary: SessionCleanupSummary;
|
||||
|
||||
@@ -23,7 +23,7 @@ import { projectSessionStoreForPersistence } from "./skill-prompt-blobs.js";
|
||||
import { shouldPreserveMaintenanceEntry } from "./store-maintenance.js";
|
||||
import type { SessionEntry } from "./types.js";
|
||||
|
||||
export type SessionDiskBudgetConfig = {
|
||||
type SessionDiskBudgetConfig = {
|
||||
maxDiskBytes: number | null;
|
||||
highWaterBytes: number | null;
|
||||
};
|
||||
@@ -46,7 +46,7 @@ export type SessionUnreferencedArtifactSweepResult = {
|
||||
olderThanMs: number;
|
||||
};
|
||||
|
||||
export type SessionDiskBudgetLogger = {
|
||||
type SessionDiskBudgetLogger = {
|
||||
warn: (message: string, context?: Record<string, unknown>) => void;
|
||||
info: (message: string, context?: Record<string, unknown>) => void;
|
||||
};
|
||||
|
||||
@@ -15,19 +15,19 @@ import {
|
||||
import { loadSessionEntry, type SessionAccessScope } from "./session-accessor.js";
|
||||
import type { SessionEntry } from "./types.js";
|
||||
|
||||
export type ResolveSessionEntryResetFreshnessParams = SessionAccessScope & {
|
||||
type ResolveSessionEntryResetFreshnessParams = SessionAccessScope & {
|
||||
now?: number;
|
||||
resetOverride?: SessionResetConfig;
|
||||
resetType: SessionResetType;
|
||||
sessionCfg?: SessionConfig;
|
||||
};
|
||||
|
||||
export type SessionEntryLifecycleTimestamps = {
|
||||
type SessionEntryLifecycleTimestamps = {
|
||||
sessionStartedAt?: number;
|
||||
lastInteractionAt?: number;
|
||||
};
|
||||
|
||||
export type ResolvedSessionEntryResetFreshness =
|
||||
type ResolvedSessionEntryResetFreshness =
|
||||
| {
|
||||
state: "missing";
|
||||
entry: undefined;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { loadSessionEntry, patchSessionEntry } from "./session-accessor.js";
|
||||
import { resolveFreshSessionTotalTokens } from "./types.js";
|
||||
import type { SessionEntry, SessionGoal, SessionGoalStatus } from "./types.js";
|
||||
|
||||
export type SessionGoalSnapshot = {
|
||||
type SessionGoalSnapshot = {
|
||||
status: "missing" | "found";
|
||||
goal?: SessionGoal;
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ type SessionWorkStartOptions = {
|
||||
|
||||
/** Stable Gateway error detail for stale session lifecycle requests. */
|
||||
export const SESSION_LIFECYCLE_CHANGED_ERROR_REASON = "session-changed";
|
||||
export const SESSION_WORK_START_INVALIDATED_ERROR_CODE = "SESSION_WORK_START_INVALIDATED";
|
||||
const SESSION_WORK_START_INVALIDATED_ERROR_CODE = "SESSION_WORK_START_INVALIDATED";
|
||||
|
||||
export class SessionWorkStartInvalidatedError extends Error {
|
||||
readonly code = SESSION_WORK_START_INVALIDATED_ERROR_CODE;
|
||||
|
||||
@@ -61,7 +61,7 @@ export function resolveSessionFilePathOptions(params: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const SAFE_SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
||||
const SAFE_SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
||||
|
||||
export function validateSessionId(sessionId: string): string {
|
||||
const trimmed = sessionId.trim();
|
||||
@@ -294,16 +294,6 @@ export function resolveSessionTranscriptPath(
|
||||
): string {
|
||||
return resolveSessionTranscriptPathInDir(sessionId, resolveAgentSessionsDir(agentId), topicId);
|
||||
}
|
||||
|
||||
export function resolveExplicitSessionFilePath(
|
||||
sessionFile: string,
|
||||
opts?: SessionFilePathOptions,
|
||||
): string {
|
||||
return resolvePathWithinSessionsDir(resolveSessionsDir(opts), sessionFile, {
|
||||
agentId: opts?.agentId,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveSessionFilePath(
|
||||
sessionId: string,
|
||||
entry?: { sessionFile?: string },
|
||||
|
||||
@@ -5,7 +5,7 @@ import { normalizeSessionEntrySlotKey } from "../../plugins/session-entry-slot-k
|
||||
import type { SessionEntry } from "./types.js";
|
||||
|
||||
/** Cleanup variants owned by plugin host lifecycle paths. */
|
||||
export type PluginHostSessionCleanupMode = "plugin-owned-state" | "promoted-slots";
|
||||
type PluginHostSessionCleanupMode = "plugin-owned-state" | "promoted-slots";
|
||||
|
||||
export type PluginHostSessionCleanupStoreParams = {
|
||||
/** Agent that owns the resolved session store target. */
|
||||
|
||||
@@ -19,11 +19,11 @@ export type SessionFreshness = {
|
||||
staleReason?: SessionResetMode;
|
||||
};
|
||||
|
||||
export const DEFAULT_RESET_MODE: SessionResetMode = "daily";
|
||||
export const DEFAULT_RESET_AT_HOUR = 4;
|
||||
const DEFAULT_RESET_MODE: SessionResetMode = "daily";
|
||||
const DEFAULT_RESET_AT_HOUR = 4;
|
||||
|
||||
/** Returns the most recent daily reset boundary for the supplied wall-clock time. */
|
||||
export function resolveDailyResetAtMs(now: number, atHour: number): number {
|
||||
function resolveDailyResetAtMs(now: number, atHour: number): number {
|
||||
const normalizedAtHour = normalizeResetAtHour(atHour);
|
||||
const resetAt = new Date(now);
|
||||
resetAt.setHours(normalizedAtHour, 0, 0, 0);
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
// Session reset tests cover clearing persisted session state.
|
||||
// Session reset tests cover conversation-aware reset classification.
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createSessionConversationTestRegistry } from "../../test-utils/session-conversation-registry.js";
|
||||
import { isThreadSessionKey, resolveSessionResetType } from "./reset.js";
|
||||
import { resolveSessionResetType } from "./reset.js";
|
||||
|
||||
describe("session reset thread detection", () => {
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(createSessionConversationTestRegistry());
|
||||
});
|
||||
|
||||
it("does not treat feishu conversation ids with embedded :topic: as thread suffixes", () => {
|
||||
const sessionKey =
|
||||
"agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user";
|
||||
expect(isThreadSessionKey(sessionKey)).toBe(false);
|
||||
expect(resolveSessionResetType({ sessionKey })).toBe("group");
|
||||
it("does not treat Feishu conversation ids with embedded :topic: as thread suffixes", () => {
|
||||
expect(
|
||||
resolveSessionResetType({
|
||||
sessionKey:
|
||||
"agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
|
||||
}),
|
||||
).toBe("group");
|
||||
});
|
||||
|
||||
it("still treats telegram :topic: suffixes as thread sessions", () => {
|
||||
const sessionKey = "agent:main:telegram:group:-100123:topic:77";
|
||||
expect(isThreadSessionKey(sessionKey)).toBe(true);
|
||||
expect(resolveSessionResetType({ sessionKey })).toBe("thread");
|
||||
it("still treats Telegram :topic: suffixes as thread sessions", () => {
|
||||
expect(
|
||||
resolveSessionResetType({ sessionKey: "agent:main:telegram:group:-100123:topic:77" }),
|
||||
).toBe("thread");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,10 +8,7 @@ import { normalizeMessageChannel } from "../../utils/message-channel.js";
|
||||
import type { SessionConfig, SessionResetConfig } from "../types.base.js";
|
||||
/** Public reset policy exports plus helpers that classify direct, group, and thread sessions. */
|
||||
export {
|
||||
DEFAULT_RESET_AT_HOUR,
|
||||
DEFAULT_RESET_MODE,
|
||||
evaluateSessionFreshness,
|
||||
resolveDailyResetAtMs,
|
||||
resolveSessionResetPolicy,
|
||||
type SessionFreshness,
|
||||
type SessionResetMode,
|
||||
@@ -23,7 +20,7 @@ import type { SessionResetType } from "./reset-policy.js";
|
||||
const GROUP_SESSION_MARKERS = [":group:", ":channel:"];
|
||||
|
||||
/** Returns true when a session key is known to represent a thread. */
|
||||
export function isThreadSessionKey(sessionKey?: string | null): boolean {
|
||||
function isThreadSessionKey(sessionKey?: string | null): boolean {
|
||||
return Boolean(resolveLoadedSessionThreadInfo(sessionKey).threadId);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ export type ReadSessionUpdatedAt = (params: {
|
||||
storePath: string;
|
||||
sessionKey: string;
|
||||
}) => number | undefined;
|
||||
|
||||
export type RecordSessionMetaFromInbound = (params: {
|
||||
storePath: string;
|
||||
sessionKey: string;
|
||||
|
||||
@@ -45,11 +45,9 @@ import {
|
||||
} from "./session-accessor.js";
|
||||
import {
|
||||
appendSqliteTranscriptEvent,
|
||||
appendSqliteTranscriptEvents,
|
||||
appendSqliteTranscriptMessage,
|
||||
branchSqliteCompactionCheckpointSession,
|
||||
cleanupSqliteSessionLifecycleArtifacts,
|
||||
deleteSqliteTranscript,
|
||||
forkSqliteSessionEntryFromParentTarget,
|
||||
listSqliteSessionEntries,
|
||||
loadExactSqliteSessionEntry,
|
||||
@@ -63,7 +61,6 @@ import {
|
||||
replaceSqliteSessionEntrySync,
|
||||
replaceSqliteTranscriptEvents,
|
||||
restoreSqliteCompactionCheckpointSession,
|
||||
sqliteTranscriptExists,
|
||||
upsertSqliteSessionEntry,
|
||||
} from "./session-accessor.sqlite.js";
|
||||
import { parseSqliteSessionFileMarker } from "./sqlite-marker.js";
|
||||
@@ -1868,40 +1865,6 @@ describe("sqlite session normalization", () => {
|
||||
expect(upsertRow?.updated_at).toBe(upsertEntry.updatedAt);
|
||||
});
|
||||
|
||||
it("replaces, appends, checks, and deletes SQLite transcript rows without filesystem artifacts", async () => {
|
||||
const env = { ...process.env, OPENCLAW_STATE_DIR: paths.stateDir };
|
||||
const scope = {
|
||||
agentId: "main",
|
||||
env,
|
||||
sessionId: "transcript-state-session",
|
||||
sessionKey: "agent:main:main",
|
||||
storePath: paths.sqlitePath,
|
||||
};
|
||||
|
||||
expect(sqliteTranscriptExists(scope)).toBe(false);
|
||||
|
||||
await replaceSqliteTranscriptEvents(scope, [
|
||||
{ type: "session", id: "transcript-state-session", cwd: paths.tempDir },
|
||||
{ type: "message", id: "msg-1", parentId: null, message: { content: "one" } },
|
||||
]);
|
||||
await appendSqliteTranscriptEvents(scope, [
|
||||
{ type: "message", id: "msg-2", parentId: "msg-1", message: { content: "two" } },
|
||||
]);
|
||||
|
||||
expect(sqliteTranscriptExists(scope)).toBe(true);
|
||||
await expect(loadSqliteTranscriptEvents(scope)).resolves.toEqual([
|
||||
{ type: "session", id: "transcript-state-session", cwd: paths.tempDir },
|
||||
{ type: "message", id: "msg-1", parentId: null, message: { content: "one" } },
|
||||
{ type: "message", id: "msg-2", parentId: "msg-1", message: { content: "two" } },
|
||||
]);
|
||||
|
||||
await expect(deleteSqliteTranscript(scope)).resolves.toBe(true);
|
||||
expect(sqliteTranscriptExists(scope)).toBe(false);
|
||||
await expect(loadSqliteTranscriptEvents(scope)).resolves.toEqual([]);
|
||||
expect(fs.existsSync(paths.sqlitePath)).toBe(true);
|
||||
expect(fs.readdirSync(paths.tempDir)).not.toContain("transcript-state-session.jsonl");
|
||||
});
|
||||
|
||||
it("branches a checkpoint by copying SQLite rows and creating the entry transactionally", async () => {
|
||||
const env = { ...process.env, OPENCLAW_STATE_DIR: paths.stateDir };
|
||||
const sourceScope = {
|
||||
|
||||
@@ -1718,37 +1718,6 @@ function parseLatestAssistantMessageEvent(
|
||||
};
|
||||
}
|
||||
|
||||
/** Checks whether the additive SQLite transcript store has rows for a transcript. */
|
||||
export function sqliteTranscriptExists(scope: SessionTranscriptReadScope): boolean {
|
||||
const resolved = resolveSqliteTranscriptReadScope(scope);
|
||||
const database = openOpenClawAgentDatabase(toDatabaseOptions(resolved));
|
||||
const db = getSessionKysely(database.db);
|
||||
const row = executeSqliteQueryTakeFirstSync(
|
||||
database.db,
|
||||
db
|
||||
.selectFrom("transcript_events")
|
||||
.select("seq")
|
||||
.where("session_id", "=", resolved.sessionId)
|
||||
.limit(1),
|
||||
);
|
||||
return row !== undefined;
|
||||
}
|
||||
|
||||
/** Deletes rows for one transcript from the additive SQLite transcript store. */
|
||||
export async function deleteSqliteTranscript(scope: SessionTranscriptReadScope): Promise<boolean> {
|
||||
const resolved = resolveSqliteTranscriptReadScope(scope);
|
||||
return await runExclusiveSqliteSessionWrite(resolved, async () => {
|
||||
let deleted = false;
|
||||
runOpenClawAgentWriteTransaction((database) => {
|
||||
deleted = deleteSqliteTranscriptEventsInTransaction(database, resolved.sessionId);
|
||||
if (deleted) {
|
||||
touchTranscriptMutationInTransaction(database, resolved.sessionId);
|
||||
}
|
||||
}, toDatabaseOptions(resolved));
|
||||
return deleted;
|
||||
});
|
||||
}
|
||||
|
||||
/** Fully replaces rows for one transcript in the additive SQLite transcript store. */
|
||||
export async function replaceSqliteTranscriptEvents(
|
||||
scope: SessionTranscriptAccessScope,
|
||||
@@ -1883,22 +1852,6 @@ export function appendSqliteTranscriptEventSync(
|
||||
}, toDatabaseOptions(resolved));
|
||||
}
|
||||
|
||||
/** Appends raw transcript events to the additive SQLite transcript store in one transaction. */
|
||||
export async function appendSqliteTranscriptEvents(
|
||||
scope: SessionTranscriptAccessScope,
|
||||
events: TranscriptEvent[],
|
||||
): Promise<void> {
|
||||
if (events.length === 0) {
|
||||
return;
|
||||
}
|
||||
const resolved = resolveSqliteTranscriptScope(scope);
|
||||
await runExclusiveSqliteSessionWrite(resolved, async () => {
|
||||
runOpenClawAgentWriteTransaction((database) => {
|
||||
appendTranscriptEventsInTransaction(database, resolved, events);
|
||||
}, toDatabaseOptions(resolved));
|
||||
});
|
||||
}
|
||||
|
||||
/** Appends a guarded transcript turn and touches its session row in one queued write. */
|
||||
export async function appendSqliteExpectedSessionTranscriptTurn(
|
||||
scope: SessionTranscriptWriteScope,
|
||||
|
||||
@@ -11,14 +11,14 @@ import { collectActiveSessionWorkAdmissionKeys } from "./store-maintenance-prese
|
||||
import { pruneStaleEntries } from "./store.js";
|
||||
import type { SessionEntry } from "./types.js";
|
||||
|
||||
export type SessionRegistryMaintenanceStoreSummary = {
|
||||
type SessionRegistryMaintenanceStoreSummary = {
|
||||
afterCount: number;
|
||||
beforeCount: number;
|
||||
preservedRunning: number;
|
||||
pruned: number;
|
||||
};
|
||||
|
||||
export type SessionRegistryMaintenanceStoreOptions = {
|
||||
type SessionRegistryMaintenanceStoreOptions = {
|
||||
/** Apply pruning to the backing store; false previews against a clone. */
|
||||
apply: boolean;
|
||||
/** Retention window for cron-run session entries. */
|
||||
|
||||
@@ -81,7 +81,7 @@ function readMessageText(message: unknown): string | undefined {
|
||||
* assistant message text is indexed; tool results, reasoning blocks, and
|
||||
* images stay out of the index by construction.
|
||||
*/
|
||||
export function extractTranscriptIndexEntry(
|
||||
function extractTranscriptIndexEntry(
|
||||
event: unknown,
|
||||
fallbackTimestamp: number,
|
||||
): TranscriptIndexEntry | undefined {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user