comisai 1.0.11 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@comis/agent/dist/model/operation-model-defaults.js +1 -0
- package/node_modules/@comis/agent/dist/session/comis-session-manager.js +9 -9
- package/node_modules/@comis/agent/package.json +1 -1
- package/node_modules/@comis/channels/package.json +1 -1
- package/node_modules/@comis/cli/package.json +1 -1
- package/node_modules/@comis/core/dist/event-bus/events-infra.d.ts +2 -0
- package/node_modules/@comis/core/package.json +1 -1
- package/node_modules/@comis/daemon/dist/wiring/setup-channels.js +12 -1
- package/node_modules/@comis/daemon/dist/wiring/setup-gateway.js +1 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-schedulers.js +1 -0
- package/node_modules/@comis/daemon/package.json +1 -1
- package/node_modules/@comis/gateway/package.json +1 -1
- package/node_modules/@comis/infra/package.json +1 -1
- package/node_modules/@comis/memory/package.json +1 -1
- package/node_modules/@comis/scheduler/dist/cron/cron-types.d.ts +6 -0
- package/node_modules/@comis/scheduler/dist/cron/cron-types.js +2 -0
- package/node_modules/@comis/scheduler/package.json +1 -1
- package/node_modules/@comis/shared/package.json +1 -1
- package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.d.ts +1 -1
- package/node_modules/@comis/skills/package.json +1 -1
- package/package.json +18 -18
- package/node_modules/@comis/core/dist/approval/approval-gate.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/approval/approval-gate.test.js +0 -1003
- package/node_modules/@comis/core/dist/bootstrap.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/bootstrap.test.js +0 -150
- package/node_modules/@comis/core/dist/config/backup.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/backup.test.js +0 -160
- package/node_modules/@comis/core/dist/config/env-substitution.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/env-substitution.test.js +0 -259
- package/node_modules/@comis/core/dist/config/field-metadata.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/field-metadata.test.js +0 -72
- package/node_modules/@comis/core/dist/config/git-manager.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/git-manager.test.js +0 -1042
- package/node_modules/@comis/core/dist/config/immutable-keys.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/immutable-keys.test.js +0 -283
- package/node_modules/@comis/core/dist/config/include-resolver.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/include-resolver.test.js +0 -292
- package/node_modules/@comis/core/dist/config/layered.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/layered.test.js +0 -211
- package/node_modules/@comis/core/dist/config/loader.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/loader.test.js +0 -300
- package/node_modules/@comis/core/dist/config/migrate.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/migrate.test.js +0 -244
- package/node_modules/@comis/core/dist/config/partial-validator.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/partial-validator.test.js +0 -98
- package/node_modules/@comis/core/dist/config/schema-agent-model.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-agent-model.test.js +0 -279
- package/node_modules/@comis/core/dist/config/schema-agent-session.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-agent-session.test.js +0 -242
- package/node_modules/@comis/core/dist/config/schema-agent.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-agent.test.js +0 -645
- package/node_modules/@comis/core/dist/config/schema-channel.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-channel.test.js +0 -341
- package/node_modules/@comis/core/dist/config/schema-coalescer.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-coalescer.test.js +0 -51
- package/node_modules/@comis/core/dist/config/schema-context-engine.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-context-engine.test.js +0 -797
- package/node_modules/@comis/core/dist/config/schema-context-guard.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-context-guard.test.js +0 -111
- package/node_modules/@comis/core/dist/config/schema-daemon.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-daemon.test.js +0 -107
- package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.js +0 -51
- package/node_modules/@comis/core/dist/config/schema-documentation.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-documentation.test.js +0 -63
- package/node_modules/@comis/core/dist/config/schema-gateway.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-gateway.test.js +0 -219
- package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.js +0 -41
- package/node_modules/@comis/core/dist/config/schema-integrations.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-integrations.test.js +0 -92
- package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.js +0 -61
- package/node_modules/@comis/core/dist/config/schema-misc.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-misc.test.js +0 -394
- package/node_modules/@comis/core/dist/config/schema-observability.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-observability.test.js +0 -76
- package/node_modules/@comis/core/dist/config/schema-providers.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-providers.test.js +0 -294
- package/node_modules/@comis/core/dist/config/schema-queue.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-queue.test.js +0 -234
- package/node_modules/@comis/core/dist/config/schema-response-prefix.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-response-prefix.test.js +0 -36
- package/node_modules/@comis/core/dist/config/schema-security.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-security.test.js +0 -54
- package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.js +0 -60
- package/node_modules/@comis/core/dist/config/schema-serializer.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-serializer.test.js +0 -73
- package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.js +0 -39
- package/node_modules/@comis/core/dist/context/context.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/context/context.test.js +0 -276
- package/node_modules/@comis/core/dist/domain/agent-response.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/agent-response.test.js +0 -159
- package/node_modules/@comis/core/dist/domain/credential-mapping.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/credential-mapping.test.js +0 -135
- package/node_modules/@comis/core/dist/domain/delivery-origin.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/delivery-origin.test.js +0 -68
- package/node_modules/@comis/core/dist/domain/execution-graph.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/execution-graph.test.js +0 -441
- package/node_modules/@comis/core/dist/domain/memory-entry.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/memory-entry.test.js +0 -193
- package/node_modules/@comis/core/dist/domain/normalized-message.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/normalized-message.test.js +0 -255
- package/node_modules/@comis/core/dist/domain/session-key.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/session-key.test.js +0 -291
- package/node_modules/@comis/core/dist/domain/subagent-context-config.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/subagent-context-config.test.js +0 -131
- package/node_modules/@comis/core/dist/domain/subagent-context-types.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/domain/subagent-context-types.test.js +0 -182
- package/node_modules/@comis/core/dist/event-bus/bus.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/event-bus/bus.test.js +0 -152
- package/node_modules/@comis/core/dist/event-bus/events-agent.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/event-bus/events-agent.test.js +0 -409
- package/node_modules/@comis/core/dist/event-bus/events-channel.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/event-bus/events-channel.test.js +0 -240
- package/node_modules/@comis/core/dist/event-bus/events-infra.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/event-bus/events-infra.test.js +0 -416
- package/node_modules/@comis/core/dist/event-bus/events-messaging.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/event-bus/events-messaging.test.js +0 -433
- package/node_modules/@comis/core/dist/event-bus/events.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/event-bus/events.test.js +0 -93
- package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.js +0 -29
- package/node_modules/@comis/core/dist/hooks/hook-runner.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/hooks/hook-runner.test.js +0 -490
- package/node_modules/@comis/core/dist/hooks/hook-strategies.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/hooks/hook-strategies.test.js +0 -209
- package/node_modules/@comis/core/dist/hooks/integration.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/hooks/integration.test.js +0 -235
- package/node_modules/@comis/core/dist/hooks/plugin-registry.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/hooks/plugin-registry.test.js +0 -470
- package/node_modules/@comis/core/dist/load-env.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/load-env.test.js +0 -21
- package/node_modules/@comis/core/dist/security/action-classifier.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/action-classifier.test.js +0 -298
- package/node_modules/@comis/core/dist/security/audit-aggregator.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/audit-aggregator.test.js +0 -128
- package/node_modules/@comis/core/dist/security/audit.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/audit.test.js +0 -154
- package/node_modules/@comis/core/dist/security/canary-token.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/canary-token.test.js +0 -45
- package/node_modules/@comis/core/dist/security/config-redaction.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/config-redaction.test.js +0 -107
- package/node_modules/@comis/core/dist/security/external-content.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/external-content.test.js +0 -210
- package/node_modules/@comis/core/dist/security/injection-patterns.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/injection-patterns.test.js +0 -213
- package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.js +0 -194
- package/node_modules/@comis/core/dist/security/input-guard.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/input-guard.test.js +0 -215
- package/node_modules/@comis/core/dist/security/input-security-guard.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/input-security-guard.test.js +0 -215
- package/node_modules/@comis/core/dist/security/input-validator.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/input-validator.test.js +0 -133
- package/node_modules/@comis/core/dist/security/log-sanitizer.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/log-sanitizer.test.js +0 -260
- package/node_modules/@comis/core/dist/security/memory-write-validator.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/memory-write-validator.test.js +0 -62
- package/node_modules/@comis/core/dist/security/output-guard.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/output-guard.test.js +0 -397
- package/node_modules/@comis/core/dist/security/safe-path.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/safe-path.test.js +0 -95
- package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.js +0 -231
- package/node_modules/@comis/core/dist/security/secret-access.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/secret-access.test.js +0 -41
- package/node_modules/@comis/core/dist/security/secret-crypto.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/secret-crypto.test.js +0 -113
- package/node_modules/@comis/core/dist/security/secret-manager.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/secret-manager.test.js +0 -394
- package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.js +0 -268
- package/node_modules/@comis/core/dist/security/secrets-audit.test.d.ts +0 -10
- package/node_modules/@comis/core/dist/security/secrets-audit.test.js +0 -295
- package/node_modules/@comis/core/dist/security/ssrf-guard.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/ssrf-guard.test.js +0 -127
- package/node_modules/@comis/core/dist/security/token-generator.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/security/token-generator.test.js +0 -35
- package/node_modules/@comis/core/dist/tool-metadata.test.d.ts +0 -1
- package/node_modules/@comis/core/dist/tool-metadata.test.js +0 -112
- package/node_modules/@comis/memory/dist/compaction.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/compaction.test.js +0 -298
- package/node_modules/@comis/memory/dist/context-schema.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/context-schema.test.js +0 -246
- package/node_modules/@comis/memory/dist/context-store.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/context-store.test.js +0 -708
- package/node_modules/@comis/memory/dist/credential-mapping-schema.test.d.ts +0 -7
- package/node_modules/@comis/memory/dist/credential-mapping-schema.test.js +0 -73
- package/node_modules/@comis/memory/dist/credential-mapping-store.test.d.ts +0 -8
- package/node_modules/@comis/memory/dist/credential-mapping-store.test.js +0 -340
- package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.js +0 -165
- package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.js +0 -263
- package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.js +0 -202
- package/node_modules/@comis/memory/dist/embedding-cache-lru.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-cache-lru.test.js +0 -241
- package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.d.ts +0 -8
- package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.js +0 -678
- package/node_modules/@comis/memory/dist/embedding-cache.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-cache.test.js +0 -213
- package/node_modules/@comis/memory/dist/embedding-fingerprint.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-fingerprint.test.js +0 -87
- package/node_modules/@comis/memory/dist/embedding-hash.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-hash.test.js +0 -31
- package/node_modules/@comis/memory/dist/embedding-integration.test.d.ts +0 -8
- package/node_modules/@comis/memory/dist/embedding-integration.test.js +0 -232
- package/node_modules/@comis/memory/dist/embedding-provider-factory.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-provider-factory.test.js +0 -176
- package/node_modules/@comis/memory/dist/embedding-provider-local.test.d.ts +0 -8
- package/node_modules/@comis/memory/dist/embedding-provider-local.test.js +0 -76
- package/node_modules/@comis/memory/dist/embedding-provider-openai.test.d.ts +0 -8
- package/node_modules/@comis/memory/dist/embedding-provider-openai.test.js +0 -100
- package/node_modules/@comis/memory/dist/embedding-queue.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/embedding-queue.test.js +0 -236
- package/node_modules/@comis/memory/dist/hybrid-search.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/hybrid-search.test.js +0 -476
- package/node_modules/@comis/memory/dist/identity-link-store.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/identity-link-store.test.js +0 -88
- package/node_modules/@comis/memory/dist/memory-api.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/memory-api.test.js +0 -495
- package/node_modules/@comis/memory/dist/memory-concurrency.test.d.ts +0 -20
- package/node_modules/@comis/memory/dist/memory-concurrency.test.js +0 -222
- package/node_modules/@comis/memory/dist/named-graph-store.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/named-graph-store.test.js +0 -227
- package/node_modules/@comis/memory/dist/observability-store.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/observability-store.test.js +0 -704
- package/node_modules/@comis/memory/dist/row-mapper.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/row-mapper.test.js +0 -409
- package/node_modules/@comis/memory/dist/schema.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/schema.test.js +0 -240
- package/node_modules/@comis/memory/dist/secret-store-schema.test.d.ts +0 -7
- package/node_modules/@comis/memory/dist/secret-store-schema.test.js +0 -90
- package/node_modules/@comis/memory/dist/session-store.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/session-store.test.js +0 -262
- package/node_modules/@comis/memory/dist/setup-secrets.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/setup-secrets.test.js +0 -140
- package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.d.ts +0 -1
- package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.js +0 -888
- package/node_modules/@comis/memory/dist/sqlite-secret-store.test.d.ts +0 -8
- package/node_modules/@comis/memory/dist/sqlite-secret-store.test.js +0 -245
- package/node_modules/@comis/shared/dist/abort.test.d.ts +0 -1
- package/node_modules/@comis/shared/dist/abort.test.js +0 -71
- package/node_modules/@comis/shared/dist/result.test.d.ts +0 -1
- package/node_modules/@comis/shared/dist/result.test.js +0 -178
- package/node_modules/@comis/shared/dist/suppress-error.test.d.ts +0 -1
- package/node_modules/@comis/shared/dist/suppress-error.test.js +0 -50
- package/node_modules/@comis/shared/dist/timeout.test.d.ts +0 -1
- package/node_modules/@comis/shared/dist/timeout.test.js +0 -75
- package/node_modules/@comis/shared/dist/ttl-cache.test.d.ts +0 -1
- package/node_modules/@comis/shared/dist/ttl-cache.test.js +0 -81
|
@@ -1,1042 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { ok, err } from "@comis/shared";
|
|
3
|
-
import { createConfigGitManager, encodeCommitMessage, GITIGNORE_CONTENT, parseCommitMessage, } from "./git-manager.js";
|
|
4
|
-
function createSha(index) {
|
|
5
|
-
return `${"a".repeat(7)}${String(index).padStart(33, "0")}`;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Create mock deps with a simulated in-memory git repo.
|
|
9
|
-
*
|
|
10
|
-
* Handles git init, add, commit, log, diff, status, rev-parse, cat-file,
|
|
11
|
-
* and checkout commands with deterministic responses.
|
|
12
|
-
*/
|
|
13
|
-
function createMockDeps(opts) {
|
|
14
|
-
const calls = [];
|
|
15
|
-
const writtenFiles = opts?.writtenFiles ?? new Map();
|
|
16
|
-
const repo = {
|
|
17
|
-
initialized: opts?.preInitialized ?? false,
|
|
18
|
-
commits: [],
|
|
19
|
-
workingTree: new Map(opts?.initialFiles ?? []),
|
|
20
|
-
stagedFiles: new Set(),
|
|
21
|
-
dirty: false,
|
|
22
|
-
};
|
|
23
|
-
// If pre-initialized, create an initial commit
|
|
24
|
-
if (opts?.preInitialized) {
|
|
25
|
-
repo.commits.push({
|
|
26
|
-
sha: createSha(0),
|
|
27
|
-
message: "Initial config snapshot",
|
|
28
|
-
timestamp: "2026-02-25T10:00:00+00:00",
|
|
29
|
-
files: new Map(repo.workingTree),
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
const execGit = async (args, cwd) => {
|
|
33
|
-
calls.push({ args, cwd });
|
|
34
|
-
const command = args[0];
|
|
35
|
-
// Check for forced failures
|
|
36
|
-
const cmdKey = args.join(" ");
|
|
37
|
-
if (opts?.failCommands) {
|
|
38
|
-
for (const [pattern, errorMsg] of opts.failCommands) {
|
|
39
|
-
if (cmdKey.includes(pattern)) {
|
|
40
|
-
return err(errorMsg);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
switch (command) {
|
|
45
|
-
case "status": {
|
|
46
|
-
if (!repo.initialized) {
|
|
47
|
-
return err("fatal: not a git repository");
|
|
48
|
-
}
|
|
49
|
-
if (repo.dirty) {
|
|
50
|
-
return ok(" M config.yaml\n");
|
|
51
|
-
}
|
|
52
|
-
return ok("");
|
|
53
|
-
}
|
|
54
|
-
case "init": {
|
|
55
|
-
repo.initialized = true;
|
|
56
|
-
return ok("Initialized empty Git repository");
|
|
57
|
-
}
|
|
58
|
-
case "add": {
|
|
59
|
-
if (!repo.initialized) {
|
|
60
|
-
return err("fatal: not a git repository");
|
|
61
|
-
}
|
|
62
|
-
// Track staged files
|
|
63
|
-
for (let i = 1; i < args.length; i++) {
|
|
64
|
-
const pattern = args[i];
|
|
65
|
-
if (pattern === "--ignore-errors")
|
|
66
|
-
continue;
|
|
67
|
-
if (pattern.includes("*")) {
|
|
68
|
-
// Glob — stage all matching files from working tree
|
|
69
|
-
const ext = pattern.replace("*", "");
|
|
70
|
-
for (const [name] of repo.workingTree) {
|
|
71
|
-
if (name.endsWith(ext)) {
|
|
72
|
-
repo.stagedFiles.add(name);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
repo.stagedFiles.add(pattern);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return ok("");
|
|
81
|
-
}
|
|
82
|
-
case "commit": {
|
|
83
|
-
if (!repo.initialized) {
|
|
84
|
-
return err("fatal: not a git repository");
|
|
85
|
-
}
|
|
86
|
-
// Extract message from -m flag
|
|
87
|
-
const mIndex = args.indexOf("-m");
|
|
88
|
-
const message = mIndex >= 0 ? args[mIndex + 1] : "";
|
|
89
|
-
// Check for --allow-empty
|
|
90
|
-
const allowEmpty = args.includes("--allow-empty");
|
|
91
|
-
if (!allowEmpty && repo.stagedFiles.size === 0 && !repo.dirty) {
|
|
92
|
-
return err("nothing to commit, working tree clean");
|
|
93
|
-
}
|
|
94
|
-
const sha = createSha(repo.commits.length);
|
|
95
|
-
const files = new Map(repo.workingTree);
|
|
96
|
-
repo.commits.push({
|
|
97
|
-
sha,
|
|
98
|
-
message,
|
|
99
|
-
timestamp: `2026-02-25T1${String(repo.commits.length).padStart(1, "0")}:00:00+00:00`,
|
|
100
|
-
files,
|
|
101
|
-
});
|
|
102
|
-
repo.stagedFiles.clear();
|
|
103
|
-
repo.dirty = false;
|
|
104
|
-
return ok(`[main ${sha.slice(0, 7)}] ${message.split("\n")[0]}`);
|
|
105
|
-
}
|
|
106
|
-
case "rev-parse": {
|
|
107
|
-
if (!repo.initialized) {
|
|
108
|
-
return err("fatal: not a git repository");
|
|
109
|
-
}
|
|
110
|
-
if (args[1] === "HEAD") {
|
|
111
|
-
if (repo.commits.length === 0) {
|
|
112
|
-
return err("fatal: ambiguous argument 'HEAD'");
|
|
113
|
-
}
|
|
114
|
-
return ok(repo.commits[repo.commits.length - 1].sha);
|
|
115
|
-
}
|
|
116
|
-
if (args[1] === "--verify" && args[2] === "HEAD~1") {
|
|
117
|
-
if (repo.commits.length < 2) {
|
|
118
|
-
return err("fatal: ambiguous argument 'HEAD~1'");
|
|
119
|
-
}
|
|
120
|
-
return ok(repo.commits[repo.commits.length - 2].sha);
|
|
121
|
-
}
|
|
122
|
-
return err(`fatal: bad revision '${args[1]}'`);
|
|
123
|
-
}
|
|
124
|
-
case "log": {
|
|
125
|
-
if (!repo.initialized || repo.commits.length === 0) {
|
|
126
|
-
return err("fatal: your current branch 'main' does not have any commits yet");
|
|
127
|
-
}
|
|
128
|
-
// Parse --max-count
|
|
129
|
-
const maxCountArg = args.find((a) => a.startsWith("--max-count="));
|
|
130
|
-
const maxCount = maxCountArg
|
|
131
|
-
? parseInt(maxCountArg.split("=")[1], 10)
|
|
132
|
-
: repo.commits.length;
|
|
133
|
-
// Build log output matching the format %H%n%aI%n%B---END---
|
|
134
|
-
const entries = [...repo.commits]
|
|
135
|
-
.reverse()
|
|
136
|
-
.slice(0, maxCount);
|
|
137
|
-
const output = entries
|
|
138
|
-
.map((c) => `${c.sha}\n${c.timestamp}\n${c.message}\n`)
|
|
139
|
-
.join("---END---");
|
|
140
|
-
return ok(output + "---END---");
|
|
141
|
-
}
|
|
142
|
-
case "diff": {
|
|
143
|
-
if (!repo.initialized) {
|
|
144
|
-
return err("fatal: not a git repository");
|
|
145
|
-
}
|
|
146
|
-
// Return a mock unified diff
|
|
147
|
-
return ok("--- a/config.yaml\n+++ b/config.yaml\n@@ -1,3 +1,3 @@\n setting: old\n-value: before\n+value: after\n other: same\n");
|
|
148
|
-
}
|
|
149
|
-
case "cat-file": {
|
|
150
|
-
if (!repo.initialized) {
|
|
151
|
-
return err("fatal: not a git repository");
|
|
152
|
-
}
|
|
153
|
-
const targetSha = args[2];
|
|
154
|
-
const found = repo.commits.find((c) => c.sha === targetSha);
|
|
155
|
-
if (found) {
|
|
156
|
-
return ok("commit");
|
|
157
|
-
}
|
|
158
|
-
return err(`fatal: Not a valid object name ${targetSha}`);
|
|
159
|
-
}
|
|
160
|
-
case "checkout": {
|
|
161
|
-
if (!repo.initialized) {
|
|
162
|
-
return err("fatal: not a git repository");
|
|
163
|
-
}
|
|
164
|
-
// Find the target commit and restore files
|
|
165
|
-
const sha = args[1];
|
|
166
|
-
const commit = repo.commits.find((c) => c.sha === sha);
|
|
167
|
-
if (commit) {
|
|
168
|
-
// Restore files from that commit
|
|
169
|
-
for (const [name, content] of commit.files) {
|
|
170
|
-
repo.workingTree.set(name, content);
|
|
171
|
-
}
|
|
172
|
-
repo.dirty = true;
|
|
173
|
-
return ok("");
|
|
174
|
-
}
|
|
175
|
-
return err(`error: pathspec '${sha}' did not match any file(s) known to git`);
|
|
176
|
-
}
|
|
177
|
-
default:
|
|
178
|
-
return ok("");
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
const deps = {
|
|
182
|
-
configDir: "/test/config",
|
|
183
|
-
execGit,
|
|
184
|
-
writeFile: async (relativePath, content) => {
|
|
185
|
-
writtenFiles.set(relativePath, content);
|
|
186
|
-
return ok(undefined);
|
|
187
|
-
},
|
|
188
|
-
};
|
|
189
|
-
return { deps, calls, repo, writtenFiles };
|
|
190
|
-
}
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
// Tests: Task 1 — init, commit, checkDirty, auto-reinit, best-effort
|
|
193
|
-
// ---------------------------------------------------------------------------
|
|
194
|
-
describe("config/git-manager", () => {
|
|
195
|
-
describe("init()", () => {
|
|
196
|
-
it("creates .git directory by running git init", async () => {
|
|
197
|
-
const { deps, calls, repo } = createMockDeps();
|
|
198
|
-
const manager = createConfigGitManager(deps);
|
|
199
|
-
const result = await manager.init();
|
|
200
|
-
expect(result.ok).toBe(true);
|
|
201
|
-
expect(repo.initialized).toBe(true);
|
|
202
|
-
// Should have called git status (check existing), git init, git add, git commit
|
|
203
|
-
const commands = calls.map((c) => c.args[0]);
|
|
204
|
-
expect(commands).toContain("status");
|
|
205
|
-
expect(commands).toContain("init");
|
|
206
|
-
expect(commands).toContain("commit");
|
|
207
|
-
});
|
|
208
|
-
it("writes .gitignore with YAML whitelist pattern", async () => {
|
|
209
|
-
const { deps, writtenFiles } = createMockDeps();
|
|
210
|
-
const manager = createConfigGitManager(deps);
|
|
211
|
-
await manager.init();
|
|
212
|
-
expect(writtenFiles.has(".gitignore")).toBe(true);
|
|
213
|
-
const content = writtenFiles.get(".gitignore");
|
|
214
|
-
expect(content).toContain("!*.yaml");
|
|
215
|
-
expect(content).toContain("!*.yml");
|
|
216
|
-
expect(content).toContain("!.gitignore");
|
|
217
|
-
expect(content).toBe(GITIGNORE_CONTENT);
|
|
218
|
-
});
|
|
219
|
-
it("creates initial commit with allow-empty", async () => {
|
|
220
|
-
const { deps, calls } = createMockDeps();
|
|
221
|
-
const manager = createConfigGitManager(deps);
|
|
222
|
-
await manager.init();
|
|
223
|
-
const commitCall = calls.find((c) => c.args[0] === "commit" && c.args.includes("--allow-empty"));
|
|
224
|
-
expect(commitCall).toBeDefined();
|
|
225
|
-
expect(commitCall.args).toContain("Initial config snapshot");
|
|
226
|
-
});
|
|
227
|
-
it("returns ok() early if repo already exists", async () => {
|
|
228
|
-
const { deps, calls } = createMockDeps({ preInitialized: true });
|
|
229
|
-
const manager = createConfigGitManager(deps);
|
|
230
|
-
const result = await manager.init();
|
|
231
|
-
expect(result.ok).toBe(true);
|
|
232
|
-
// Should only call status (which succeeds) — no init needed
|
|
233
|
-
const commands = calls.map((c) => c.args[0]);
|
|
234
|
-
expect(commands).not.toContain("init");
|
|
235
|
-
});
|
|
236
|
-
it("is idempotent — second call is a no-op", async () => {
|
|
237
|
-
const { deps, calls } = createMockDeps();
|
|
238
|
-
const manager = createConfigGitManager(deps);
|
|
239
|
-
await manager.init();
|
|
240
|
-
const callsAfterFirst = calls.length;
|
|
241
|
-
const result = await manager.init();
|
|
242
|
-
expect(result.ok).toBe(true);
|
|
243
|
-
// Second call should not make any new git calls (cached initialized flag)
|
|
244
|
-
expect(calls.length).toBe(callsAfterFirst);
|
|
245
|
-
});
|
|
246
|
-
it("returns err() on git init failure", async () => {
|
|
247
|
-
const failCommands = new Map([["init", "fatal: permission denied"]]);
|
|
248
|
-
const { deps } = createMockDeps({ failCommands });
|
|
249
|
-
const manager = createConfigGitManager(deps);
|
|
250
|
-
const result = await manager.init();
|
|
251
|
-
expect(result.ok).toBe(false);
|
|
252
|
-
if (!result.ok) {
|
|
253
|
-
expect(result.error).toContain("git init failed");
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
describe("commit()", () => {
|
|
258
|
-
it("stages YAML files and creates commit with structured metadata", async () => {
|
|
259
|
-
const initialFiles = new Map([["config.yaml", "setting: value"]]);
|
|
260
|
-
const { deps, calls, repo } = createMockDeps({
|
|
261
|
-
preInitialized: true,
|
|
262
|
-
initialFiles,
|
|
263
|
-
});
|
|
264
|
-
// Mark as dirty so commit succeeds
|
|
265
|
-
repo.dirty = true;
|
|
266
|
-
const manager = createConfigGitManager(deps);
|
|
267
|
-
const result = await manager.commit({
|
|
268
|
-
section: "agent",
|
|
269
|
-
key: "agent.model",
|
|
270
|
-
summary: "Changed model to claude-opus",
|
|
271
|
-
});
|
|
272
|
-
expect(result.ok).toBe(true);
|
|
273
|
-
if (result.ok) {
|
|
274
|
-
expect(result.value).toBeTruthy();
|
|
275
|
-
expect(result.value.length).toBeGreaterThan(0);
|
|
276
|
-
}
|
|
277
|
-
// Verify commit message contains structured metadata
|
|
278
|
-
const commitCall = calls.find((c) => c.args[0] === "commit" && !c.args.includes("--allow-empty"));
|
|
279
|
-
expect(commitCall).toBeDefined();
|
|
280
|
-
const message = commitCall.args[commitCall.args.indexOf("-m") + 1];
|
|
281
|
-
expect(message).toContain("config: Changed model to claude-opus");
|
|
282
|
-
expect(message).toContain("[section] agent");
|
|
283
|
-
expect(message).toContain("[key] agent.model");
|
|
284
|
-
});
|
|
285
|
-
it("returns commit SHA on success", async () => {
|
|
286
|
-
const { deps, repo } = createMockDeps({ preInitialized: true });
|
|
287
|
-
repo.dirty = true;
|
|
288
|
-
const manager = createConfigGitManager(deps);
|
|
289
|
-
const result = await manager.commit({
|
|
290
|
-
section: "gateway",
|
|
291
|
-
summary: "Updated rate limit",
|
|
292
|
-
});
|
|
293
|
-
expect(result.ok).toBe(true);
|
|
294
|
-
if (result.ok) {
|
|
295
|
-
// SHA should be non-empty
|
|
296
|
-
expect(result.value.length).toBeGreaterThan(0);
|
|
297
|
-
expect(result.value).not.toBe("");
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
it("triggers lazy init when repo is not initialized", async () => {
|
|
301
|
-
const { deps, calls, repo } = createMockDeps();
|
|
302
|
-
// After init, mark dirty so commit succeeds
|
|
303
|
-
const originalExecGit = deps.execGit;
|
|
304
|
-
deps.execGit = async (args, cwd) => {
|
|
305
|
-
const result = await originalExecGit(args, cwd);
|
|
306
|
-
// After init completes, make the repo dirty for the actual commit
|
|
307
|
-
if (args[0] === "commit" && args.includes("--allow-empty")) {
|
|
308
|
-
repo.dirty = true;
|
|
309
|
-
}
|
|
310
|
-
return result;
|
|
311
|
-
};
|
|
312
|
-
const manager = createConfigGitManager(deps);
|
|
313
|
-
const result = await manager.commit({
|
|
314
|
-
section: "agent",
|
|
315
|
-
summary: "First change",
|
|
316
|
-
});
|
|
317
|
-
expect(result.ok).toBe(true);
|
|
318
|
-
// Should have called init first
|
|
319
|
-
const commands = calls.map((c) => c.args[0]);
|
|
320
|
-
expect(commands).toContain("init");
|
|
321
|
-
});
|
|
322
|
-
it("returns ok('') when nothing to commit", async () => {
|
|
323
|
-
const { deps } = createMockDeps({ preInitialized: true });
|
|
324
|
-
const manager = createConfigGitManager(deps);
|
|
325
|
-
const result = await manager.commit({
|
|
326
|
-
section: "agent",
|
|
327
|
-
summary: "No changes",
|
|
328
|
-
});
|
|
329
|
-
expect(result.ok).toBe(true);
|
|
330
|
-
if (result.ok) {
|
|
331
|
-
expect(result.value).toBe("");
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
it("omits optional metadata fields when undefined", async () => {
|
|
335
|
-
const { deps, calls, repo } = createMockDeps({ preInitialized: true });
|
|
336
|
-
repo.dirty = true;
|
|
337
|
-
const manager = createConfigGitManager(deps);
|
|
338
|
-
await manager.commit({
|
|
339
|
-
section: "daemon",
|
|
340
|
-
summary: "Updated logging level",
|
|
341
|
-
});
|
|
342
|
-
const commitCall = calls.find((c) => c.args[0] === "commit" && !c.args.includes("--allow-empty"));
|
|
343
|
-
expect(commitCall).toBeDefined();
|
|
344
|
-
const message = commitCall.args[commitCall.args.indexOf("-m") + 1];
|
|
345
|
-
expect(message).toContain("[section] daemon");
|
|
346
|
-
expect(message).not.toContain("[key]");
|
|
347
|
-
expect(message).not.toContain("[agent]");
|
|
348
|
-
expect(message).not.toContain("[user]");
|
|
349
|
-
expect(message).not.toContain("[trace]");
|
|
350
|
-
});
|
|
351
|
-
it("includes all optional metadata when provided", async () => {
|
|
352
|
-
const { deps, calls, repo } = createMockDeps({ preInitialized: true });
|
|
353
|
-
repo.dirty = true;
|
|
354
|
-
const manager = createConfigGitManager(deps);
|
|
355
|
-
await manager.commit({
|
|
356
|
-
section: "agent",
|
|
357
|
-
key: "agent.model",
|
|
358
|
-
agent: "assistant-1",
|
|
359
|
-
user: "admin",
|
|
360
|
-
traceId: "trace-abc-123",
|
|
361
|
-
summary: "Full metadata commit",
|
|
362
|
-
});
|
|
363
|
-
const commitCall = calls.find((c) => c.args[0] === "commit" && !c.args.includes("--allow-empty"));
|
|
364
|
-
const message = commitCall.args[commitCall.args.indexOf("-m") + 1];
|
|
365
|
-
expect(message).toContain("[section] agent");
|
|
366
|
-
expect(message).toContain("[key] agent.model");
|
|
367
|
-
expect(message).toContain("[agent] assistant-1");
|
|
368
|
-
expect(message).toContain("[user] admin");
|
|
369
|
-
expect(message).toContain("[trace] trace-abc-123");
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
describe("checkDirty()", () => {
|
|
373
|
-
it("returns true when YAML files have uncommitted changes", async () => {
|
|
374
|
-
const { deps, repo } = createMockDeps({ preInitialized: true });
|
|
375
|
-
repo.dirty = true;
|
|
376
|
-
const manager = createConfigGitManager(deps);
|
|
377
|
-
const result = await manager.checkDirty();
|
|
378
|
-
expect(result.ok).toBe(true);
|
|
379
|
-
if (result.ok) {
|
|
380
|
-
expect(result.value).toBe(true);
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
it("returns false when working tree is clean", async () => {
|
|
384
|
-
const { deps } = createMockDeps({ preInitialized: true });
|
|
385
|
-
const manager = createConfigGitManager(deps);
|
|
386
|
-
const result = await manager.checkDirty();
|
|
387
|
-
expect(result.ok).toBe(true);
|
|
388
|
-
if (result.ok) {
|
|
389
|
-
expect(result.value).toBe(false);
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
it("calls init() first to ensure repo exists", async () => {
|
|
393
|
-
const { deps, calls } = createMockDeps();
|
|
394
|
-
const manager = createConfigGitManager(deps);
|
|
395
|
-
await manager.checkDirty();
|
|
396
|
-
// Should have init-related calls before status
|
|
397
|
-
const commands = calls.map((c) => c.args[0]);
|
|
398
|
-
expect(commands).toContain("init");
|
|
399
|
-
});
|
|
400
|
-
});
|
|
401
|
-
describe("auto-reinitialize", () => {
|
|
402
|
-
it("re-initializes when git command returns 'not a git repository'", async () => {
|
|
403
|
-
let callCount = 0;
|
|
404
|
-
const warnCalls = [];
|
|
405
|
-
const writtenFiles = new Map();
|
|
406
|
-
const mockLogger = {
|
|
407
|
-
debug: vi.fn(),
|
|
408
|
-
warn: (...args) => {
|
|
409
|
-
warnCalls.push(args);
|
|
410
|
-
},
|
|
411
|
-
};
|
|
412
|
-
// Start initialized so init() succeeds, but then fail on first status
|
|
413
|
-
// command from checkDirty with "not a git repository"
|
|
414
|
-
let removeDirCalled = false;
|
|
415
|
-
const execGit = async (args) => {
|
|
416
|
-
callCount++;
|
|
417
|
-
// First status check for init — succeed (pretend repo exists)
|
|
418
|
-
if (args[0] === "status" && callCount === 1) {
|
|
419
|
-
return ok("");
|
|
420
|
-
}
|
|
421
|
-
// Second status check from checkDirty via execWithReinit — fail
|
|
422
|
-
if (args[0] === "status" && callCount === 2) {
|
|
423
|
-
return err("fatal: not a git repository (or any of the parent directories): .git");
|
|
424
|
-
}
|
|
425
|
-
// After reinit: status check in initRepo — fail (since we "removed" .git)
|
|
426
|
-
if (args[0] === "status" && callCount > 2 && !removeDirCalled) {
|
|
427
|
-
return err("fatal: not a git repository");
|
|
428
|
-
}
|
|
429
|
-
// Handle init after reinit
|
|
430
|
-
if (args[0] === "init") {
|
|
431
|
-
return ok("Initialized empty Git repository");
|
|
432
|
-
}
|
|
433
|
-
// Handle add, commit, etc.
|
|
434
|
-
if (args[0] === "add")
|
|
435
|
-
return ok("");
|
|
436
|
-
if (args[0] === "commit")
|
|
437
|
-
return ok("[main abc1234] Initial config snapshot");
|
|
438
|
-
// After reinit, status works
|
|
439
|
-
if (args[0] === "status") {
|
|
440
|
-
return ok("");
|
|
441
|
-
}
|
|
442
|
-
return ok("");
|
|
443
|
-
};
|
|
444
|
-
const deps = {
|
|
445
|
-
configDir: "/test/config",
|
|
446
|
-
execGit,
|
|
447
|
-
writeFile: async (rel, content) => {
|
|
448
|
-
writtenFiles.set(rel, content);
|
|
449
|
-
return ok(undefined);
|
|
450
|
-
},
|
|
451
|
-
removeDir: async () => {
|
|
452
|
-
removeDirCalled = true;
|
|
453
|
-
return ok(undefined);
|
|
454
|
-
},
|
|
455
|
-
logger: mockLogger,
|
|
456
|
-
};
|
|
457
|
-
const manager = createConfigGitManager(deps);
|
|
458
|
-
// First call succeeds (init)
|
|
459
|
-
const initResult = await manager.init();
|
|
460
|
-
expect(initResult.ok).toBe(true);
|
|
461
|
-
// checkDirty triggers the "not a git repository" error and auto-reinit
|
|
462
|
-
const dirtyResult = await manager.checkDirty();
|
|
463
|
-
// Should have logged warning
|
|
464
|
-
expect(warnCalls.length).toBeGreaterThan(0);
|
|
465
|
-
const warnObj = warnCalls[0];
|
|
466
|
-
expect(warnObj[0].hint).toContain("re-initialized");
|
|
467
|
-
});
|
|
468
|
-
});
|
|
469
|
-
describe("best-effort (never throw)", () => {
|
|
470
|
-
it("init() returns err() on git failure, never throws", async () => {
|
|
471
|
-
const failCommands = new Map([["init", "fatal: permission denied"]]);
|
|
472
|
-
const { deps } = createMockDeps({ failCommands });
|
|
473
|
-
const manager = createConfigGitManager(deps);
|
|
474
|
-
const result = await manager.init();
|
|
475
|
-
expect(result.ok).toBe(false);
|
|
476
|
-
// No exception thrown — we got here
|
|
477
|
-
});
|
|
478
|
-
it("commit() returns err() on git failure, never throws", async () => {
|
|
479
|
-
const failCommands = new Map([
|
|
480
|
-
["commit -m", "fatal: unable to create commit"],
|
|
481
|
-
]);
|
|
482
|
-
const { deps, repo } = createMockDeps({
|
|
483
|
-
preInitialized: true,
|
|
484
|
-
failCommands,
|
|
485
|
-
});
|
|
486
|
-
repo.dirty = true;
|
|
487
|
-
const manager = createConfigGitManager(deps);
|
|
488
|
-
const result = await manager.commit({
|
|
489
|
-
section: "test",
|
|
490
|
-
summary: "Should fail",
|
|
491
|
-
});
|
|
492
|
-
expect(result.ok).toBe(false);
|
|
493
|
-
if (!result.ok) {
|
|
494
|
-
expect(result.error).toContain("unable to create commit");
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
it("checkDirty() returns err() on git failure, never throws", async () => {
|
|
498
|
-
const failCommands = new Map([
|
|
499
|
-
["status --porcelain", "fatal: bad config"],
|
|
500
|
-
]);
|
|
501
|
-
const { deps } = createMockDeps({
|
|
502
|
-
preInitialized: true,
|
|
503
|
-
failCommands,
|
|
504
|
-
});
|
|
505
|
-
const manager = createConfigGitManager(deps);
|
|
506
|
-
const result = await manager.checkDirty();
|
|
507
|
-
expect(result.ok).toBe(false);
|
|
508
|
-
});
|
|
509
|
-
});
|
|
510
|
-
describe("parseCommitMessage()", () => {
|
|
511
|
-
it("extracts summary from 'config: ' prefix", () => {
|
|
512
|
-
const body = "config: Updated model\n\n[section] agent";
|
|
513
|
-
const meta = parseCommitMessage(body);
|
|
514
|
-
expect(meta.summary).toBe("Updated model");
|
|
515
|
-
});
|
|
516
|
-
it("extracts all metadata tags", () => {
|
|
517
|
-
const body = [
|
|
518
|
-
"config: Full metadata",
|
|
519
|
-
"",
|
|
520
|
-
"[section] agent",
|
|
521
|
-
"[key] agent.model",
|
|
522
|
-
"[agent] assistant-1",
|
|
523
|
-
"[user] admin",
|
|
524
|
-
"[trace] trace-123",
|
|
525
|
-
].join("\n");
|
|
526
|
-
const meta = parseCommitMessage(body);
|
|
527
|
-
expect(meta.section).toBe("agent");
|
|
528
|
-
expect(meta.key).toBe("agent.model");
|
|
529
|
-
expect(meta.agent).toBe("assistant-1");
|
|
530
|
-
expect(meta.user).toBe("admin");
|
|
531
|
-
expect(meta.traceId).toBe("trace-123");
|
|
532
|
-
});
|
|
533
|
-
it("uses 'unknown' section when no [section] tag found", () => {
|
|
534
|
-
const meta = parseCommitMessage("Initial config snapshot");
|
|
535
|
-
expect(meta.section).toBe("unknown");
|
|
536
|
-
});
|
|
537
|
-
it("handles commit messages without 'config: ' prefix", () => {
|
|
538
|
-
const meta = parseCommitMessage("Initial config snapshot");
|
|
539
|
-
expect(meta.summary).toBe("Initial config snapshot");
|
|
540
|
-
});
|
|
541
|
-
});
|
|
542
|
-
// -----------------------------------------------------------------------
|
|
543
|
-
// Tests: Task 2 — history, diff, rollback
|
|
544
|
-
// -----------------------------------------------------------------------
|
|
545
|
-
describe("history()", () => {
|
|
546
|
-
it("returns up to 10 entries by default, newest first", async () => {
|
|
547
|
-
const { deps, repo } = createMockDeps({ preInitialized: true });
|
|
548
|
-
// Create several commits
|
|
549
|
-
for (let i = 1; i <= 5; i++) {
|
|
550
|
-
repo.commits.push({
|
|
551
|
-
sha: createSha(i),
|
|
552
|
-
message: `config: Change ${i}\n\n[section] agent`,
|
|
553
|
-
timestamp: `2026-02-25T1${i}:00:00+00:00`,
|
|
554
|
-
files: new Map(),
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
const manager = createConfigGitManager(deps);
|
|
558
|
-
const result = await manager.history();
|
|
559
|
-
expect(result.ok).toBe(true);
|
|
560
|
-
if (result.ok) {
|
|
561
|
-
// Should include the initial commit + 5 added = 6 total
|
|
562
|
-
expect(result.value.length).toBe(6);
|
|
563
|
-
// Newest first (reversed in our mock)
|
|
564
|
-
expect(result.value[0].metadata.summary).toBe("Change 5");
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
it("respects limit option", async () => {
|
|
568
|
-
const { deps, repo } = createMockDeps({ preInitialized: true });
|
|
569
|
-
for (let i = 1; i <= 10; i++) {
|
|
570
|
-
repo.commits.push({
|
|
571
|
-
sha: createSha(i),
|
|
572
|
-
message: `config: Change ${i}\n\n[section] agent`,
|
|
573
|
-
timestamp: `2026-02-25T1${i}:00:00+00:00`,
|
|
574
|
-
files: new Map(),
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
const manager = createConfigGitManager(deps);
|
|
578
|
-
const result = await manager.history({ limit: 5 });
|
|
579
|
-
expect(result.ok).toBe(true);
|
|
580
|
-
if (result.ok) {
|
|
581
|
-
expect(result.value.length).toBe(5);
|
|
582
|
-
}
|
|
583
|
-
});
|
|
584
|
-
it("filters by section when specified", async () => {
|
|
585
|
-
const { deps, repo } = createMockDeps({ preInitialized: true });
|
|
586
|
-
repo.commits.push({
|
|
587
|
-
sha: createSha(1),
|
|
588
|
-
message: "config: Agent change\n\n[section] agent",
|
|
589
|
-
timestamp: "2026-02-25T11:00:00+00:00",
|
|
590
|
-
files: new Map(),
|
|
591
|
-
});
|
|
592
|
-
repo.commits.push({
|
|
593
|
-
sha: createSha(2),
|
|
594
|
-
message: "config: Gateway change\n\n[section] gateway",
|
|
595
|
-
timestamp: "2026-02-25T12:00:00+00:00",
|
|
596
|
-
files: new Map(),
|
|
597
|
-
});
|
|
598
|
-
repo.commits.push({
|
|
599
|
-
sha: createSha(3),
|
|
600
|
-
message: "config: Another agent change\n\n[section] agent",
|
|
601
|
-
timestamp: "2026-02-25T13:00:00+00:00",
|
|
602
|
-
files: new Map(),
|
|
603
|
-
});
|
|
604
|
-
const manager = createConfigGitManager(deps);
|
|
605
|
-
const result = await manager.history({ section: "agent" });
|
|
606
|
-
expect(result.ok).toBe(true);
|
|
607
|
-
if (result.ok) {
|
|
608
|
-
// Only agent entries (not gateway, not initial which has section "unknown")
|
|
609
|
-
expect(result.value.every((e) => e.metadata.section === "agent")).toBe(true);
|
|
610
|
-
expect(result.value.length).toBe(2);
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
it("returns ok([]) on empty repo (no commits besides initial)", async () => {
|
|
614
|
-
// Create a mock where log returns "does not have any commits"
|
|
615
|
-
const failCommands = new Map([
|
|
616
|
-
[
|
|
617
|
-
"log",
|
|
618
|
-
"fatal: your current branch 'main' does not have any commits yet",
|
|
619
|
-
],
|
|
620
|
-
]);
|
|
621
|
-
const { deps } = createMockDeps({
|
|
622
|
-
preInitialized: true,
|
|
623
|
-
failCommands,
|
|
624
|
-
});
|
|
625
|
-
const manager = createConfigGitManager(deps);
|
|
626
|
-
const result = await manager.history();
|
|
627
|
-
expect(result.ok).toBe(true);
|
|
628
|
-
if (result.ok) {
|
|
629
|
-
expect(result.value).toEqual([]);
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
it("parses structured metadata from commit messages", async () => {
|
|
633
|
-
const { deps, repo } = createMockDeps({ preInitialized: true });
|
|
634
|
-
repo.commits.push({
|
|
635
|
-
sha: createSha(1),
|
|
636
|
-
message: "config: Updated model\n\n[section] agent\n[key] agent.model\n[agent] assistant-1\n[user] admin\n[trace] trace-abc",
|
|
637
|
-
timestamp: "2026-02-25T11:00:00+00:00",
|
|
638
|
-
files: new Map(),
|
|
639
|
-
});
|
|
640
|
-
const manager = createConfigGitManager(deps);
|
|
641
|
-
const result = await manager.history();
|
|
642
|
-
expect(result.ok).toBe(true);
|
|
643
|
-
if (result.ok) {
|
|
644
|
-
const entry = result.value[0];
|
|
645
|
-
expect(entry.metadata.section).toBe("agent");
|
|
646
|
-
expect(entry.metadata.key).toBe("agent.model");
|
|
647
|
-
expect(entry.metadata.agent).toBe("assistant-1");
|
|
648
|
-
expect(entry.metadata.user).toBe("admin");
|
|
649
|
-
expect(entry.metadata.traceId).toBe("trace-abc");
|
|
650
|
-
expect(entry.metadata.summary).toBe("Updated model");
|
|
651
|
-
}
|
|
652
|
-
});
|
|
653
|
-
});
|
|
654
|
-
describe("diff()", () => {
|
|
655
|
-
it("returns unified diff of last commit vs previous when no SHA given", async () => {
|
|
656
|
-
const { deps, repo } = createMockDeps({ preInitialized: true });
|
|
657
|
-
// Add a second commit so HEAD~1 exists
|
|
658
|
-
repo.commits.push({
|
|
659
|
-
sha: createSha(1),
|
|
660
|
-
message: "config: Second commit",
|
|
661
|
-
timestamp: "2026-02-25T11:00:00+00:00",
|
|
662
|
-
files: new Map(),
|
|
663
|
-
});
|
|
664
|
-
const manager = createConfigGitManager(deps);
|
|
665
|
-
const result = await manager.diff();
|
|
666
|
-
expect(result.ok).toBe(true);
|
|
667
|
-
if (result.ok) {
|
|
668
|
-
// Mock returns a unified diff format
|
|
669
|
-
expect(result.value).toContain("---");
|
|
670
|
-
expect(result.value).toContain("+++");
|
|
671
|
-
expect(result.value).toContain("@@");
|
|
672
|
-
}
|
|
673
|
-
});
|
|
674
|
-
it("returns diff from specified SHA to HEAD", async () => {
|
|
675
|
-
const { deps, repo, calls } = createMockDeps({ preInitialized: true });
|
|
676
|
-
repo.commits.push({
|
|
677
|
-
sha: createSha(1),
|
|
678
|
-
message: "config: Second commit",
|
|
679
|
-
timestamp: "2026-02-25T11:00:00+00:00",
|
|
680
|
-
files: new Map(),
|
|
681
|
-
});
|
|
682
|
-
const manager = createConfigGitManager(deps);
|
|
683
|
-
const targetSha = createSha(0);
|
|
684
|
-
const result = await manager.diff(targetSha);
|
|
685
|
-
expect(result.ok).toBe(true);
|
|
686
|
-
// Verify diff was called with the specified SHA
|
|
687
|
-
const diffCall = calls.find((c) => c.args[0] === "diff" && c.args.some((a) => a.includes(targetSha)));
|
|
688
|
-
expect(diffCall).toBeDefined();
|
|
689
|
-
});
|
|
690
|
-
it("returns ok('') on single-commit repo (no previous)", async () => {
|
|
691
|
-
const { deps } = createMockDeps({ preInitialized: true });
|
|
692
|
-
const manager = createConfigGitManager(deps);
|
|
693
|
-
const result = await manager.diff();
|
|
694
|
-
expect(result.ok).toBe(true);
|
|
695
|
-
if (result.ok) {
|
|
696
|
-
expect(result.value).toBe("");
|
|
697
|
-
}
|
|
698
|
-
});
|
|
699
|
-
it("uses -U3 for 3 context lines", async () => {
|
|
700
|
-
const { deps, repo, calls } = createMockDeps({ preInitialized: true });
|
|
701
|
-
repo.commits.push({
|
|
702
|
-
sha: createSha(1),
|
|
703
|
-
message: "config: Second commit",
|
|
704
|
-
timestamp: "2026-02-25T11:00:00+00:00",
|
|
705
|
-
files: new Map(),
|
|
706
|
-
});
|
|
707
|
-
const manager = createConfigGitManager(deps);
|
|
708
|
-
await manager.diff();
|
|
709
|
-
const diffCall = calls.find((c) => c.args[0] === "diff");
|
|
710
|
-
expect(diffCall).toBeDefined();
|
|
711
|
-
expect(diffCall.args).toContain("-U3");
|
|
712
|
-
});
|
|
713
|
-
});
|
|
714
|
-
describe("rollback()", () => {
|
|
715
|
-
it("restores files from target SHA and creates forward commit", async () => {
|
|
716
|
-
const { deps, repo, calls } = createMockDeps({ preInitialized: true });
|
|
717
|
-
// Add commits with different file states
|
|
718
|
-
const filesV1 = new Map([["config.yaml", "version: 1"]]);
|
|
719
|
-
repo.commits.push({
|
|
720
|
-
sha: createSha(1),
|
|
721
|
-
message: "config: Version 1\n\n[section] agent",
|
|
722
|
-
timestamp: "2026-02-25T11:00:00+00:00",
|
|
723
|
-
files: filesV1,
|
|
724
|
-
});
|
|
725
|
-
const filesV2 = new Map([["config.yaml", "version: 2"]]);
|
|
726
|
-
repo.commits.push({
|
|
727
|
-
sha: createSha(2),
|
|
728
|
-
message: "config: Version 2\n\n[section] agent",
|
|
729
|
-
timestamp: "2026-02-25T12:00:00+00:00",
|
|
730
|
-
files: filesV2,
|
|
731
|
-
});
|
|
732
|
-
// Also set working tree to v2
|
|
733
|
-
repo.workingTree.set("config.yaml", "version: 2");
|
|
734
|
-
const manager = createConfigGitManager(deps);
|
|
735
|
-
const targetSha = createSha(1);
|
|
736
|
-
const result = await manager.rollback(targetSha);
|
|
737
|
-
expect(result.ok).toBe(true);
|
|
738
|
-
if (result.ok) {
|
|
739
|
-
expect(result.value).toBeTruthy();
|
|
740
|
-
}
|
|
741
|
-
// Verify checkout was called with the target SHA
|
|
742
|
-
const checkoutCall = calls.find((c) => c.args[0] === "checkout" && c.args[1] === targetSha);
|
|
743
|
-
expect(checkoutCall).toBeDefined();
|
|
744
|
-
// Verify a forward commit was created (not a reset)
|
|
745
|
-
const commitCalls = calls.filter((c) => c.args[0] === "commit" && !c.args.includes("--allow-empty"));
|
|
746
|
-
expect(commitCalls.length).toBeGreaterThan(0);
|
|
747
|
-
const rollbackCommit = commitCalls[commitCalls.length - 1];
|
|
748
|
-
const message = rollbackCommit.args[rollbackCommit.args.indexOf("-m") + 1];
|
|
749
|
-
expect(message).toContain("rollback to");
|
|
750
|
-
});
|
|
751
|
-
it("returns new commit SHA after rollback", async () => {
|
|
752
|
-
const { deps, repo } = createMockDeps({ preInitialized: true });
|
|
753
|
-
repo.commits.push({
|
|
754
|
-
sha: createSha(1),
|
|
755
|
-
message: "config: V1\n\n[section] agent",
|
|
756
|
-
timestamp: "2026-02-25T11:00:00+00:00",
|
|
757
|
-
files: new Map([["config.yaml", "v1"]]),
|
|
758
|
-
});
|
|
759
|
-
repo.workingTree.set("config.yaml", "v2");
|
|
760
|
-
const manager = createConfigGitManager(deps);
|
|
761
|
-
const result = await manager.rollback(createSha(1));
|
|
762
|
-
expect(result.ok).toBe(true);
|
|
763
|
-
if (result.ok) {
|
|
764
|
-
// Should return a SHA (new forward commit)
|
|
765
|
-
expect(result.value.length).toBeGreaterThan(0);
|
|
766
|
-
}
|
|
767
|
-
});
|
|
768
|
-
it("returns err() with invalid SHA", async () => {
|
|
769
|
-
const { deps } = createMockDeps({ preInitialized: true });
|
|
770
|
-
const manager = createConfigGitManager(deps);
|
|
771
|
-
const result = await manager.rollback("invalid-sha-does-not-exist");
|
|
772
|
-
expect(result.ok).toBe(false);
|
|
773
|
-
if (!result.ok) {
|
|
774
|
-
expect(result.error).toContain("Invalid SHA");
|
|
775
|
-
}
|
|
776
|
-
});
|
|
777
|
-
it("preserves history after rollback (forward commit, not reset)", async () => {
|
|
778
|
-
const { deps, repo } = createMockDeps({ preInitialized: true });
|
|
779
|
-
const sha1 = createSha(1);
|
|
780
|
-
repo.commits.push({
|
|
781
|
-
sha: sha1,
|
|
782
|
-
message: "config: V1\n\n[section] agent",
|
|
783
|
-
timestamp: "2026-02-25T11:00:00+00:00",
|
|
784
|
-
files: new Map([["config.yaml", "v1"]]),
|
|
785
|
-
});
|
|
786
|
-
const sha2 = createSha(2);
|
|
787
|
-
repo.commits.push({
|
|
788
|
-
sha: sha2,
|
|
789
|
-
message: "config: V2\n\n[section] agent",
|
|
790
|
-
timestamp: "2026-02-25T12:00:00+00:00",
|
|
791
|
-
files: new Map([["config.yaml", "v2"]]),
|
|
792
|
-
});
|
|
793
|
-
repo.workingTree.set("config.yaml", "v2");
|
|
794
|
-
const manager = createConfigGitManager(deps);
|
|
795
|
-
await manager.rollback(sha1);
|
|
796
|
-
// History should include all commits: initial + v1 + v2 + rollback
|
|
797
|
-
const historyResult = await manager.history({ limit: 10 });
|
|
798
|
-
expect(historyResult.ok).toBe(true);
|
|
799
|
-
if (historyResult.ok) {
|
|
800
|
-
// At least 4 commits (initial + v1 + v2 + rollback)
|
|
801
|
-
expect(historyResult.value.length).toBeGreaterThanOrEqual(4);
|
|
802
|
-
// The rollback commit should contain "rollback" in its summary
|
|
803
|
-
const rollbackEntry = historyResult.value.find((e) => e.metadata.summary.includes("rollback"));
|
|
804
|
-
expect(rollbackEntry).toBeDefined();
|
|
805
|
-
}
|
|
806
|
-
});
|
|
807
|
-
it("includes [section] * in rollback commit metadata", async () => {
|
|
808
|
-
const { deps, repo, calls } = createMockDeps({ preInitialized: true });
|
|
809
|
-
repo.commits.push({
|
|
810
|
-
sha: createSha(1),
|
|
811
|
-
message: "config: V1\n\n[section] agent",
|
|
812
|
-
timestamp: "2026-02-25T11:00:00+00:00",
|
|
813
|
-
files: new Map([["config.yaml", "v1"]]),
|
|
814
|
-
});
|
|
815
|
-
repo.workingTree.set("config.yaml", "v2");
|
|
816
|
-
const manager = createConfigGitManager(deps);
|
|
817
|
-
await manager.rollback(createSha(1));
|
|
818
|
-
const commitCalls = calls.filter((c) => c.args[0] === "commit" &&
|
|
819
|
-
!c.args.includes("--allow-empty"));
|
|
820
|
-
const rollbackCommit = commitCalls[commitCalls.length - 1];
|
|
821
|
-
const message = rollbackCommit.args[rollbackCommit.args.indexOf("-m") + 1];
|
|
822
|
-
expect(message).toContain("[section] *");
|
|
823
|
-
});
|
|
824
|
-
});
|
|
825
|
-
// -----------------------------------------------------------------------
|
|
826
|
-
// Tests: gc and squash (GITGC-01, GITGC-02)
|
|
827
|
-
// -----------------------------------------------------------------------
|
|
828
|
-
describe("gc()", () => {
|
|
829
|
-
it("runs git garbage collection", async () => {
|
|
830
|
-
const { deps, calls } = createMockDeps({ preInitialized: true });
|
|
831
|
-
const manager = createConfigGitManager(deps);
|
|
832
|
-
const result = await manager.gc();
|
|
833
|
-
expect(result.ok).toBe(true);
|
|
834
|
-
if (result.ok) {
|
|
835
|
-
expect(result.value.prunedObjects).toBe(true);
|
|
836
|
-
}
|
|
837
|
-
// Verify gc was called with --aggressive --prune=now
|
|
838
|
-
const gcCall = calls.find((c) => c.args[0] === "gc" &&
|
|
839
|
-
c.args.includes("--aggressive") &&
|
|
840
|
-
c.args.includes("--prune=now"));
|
|
841
|
-
expect(gcCall).toBeDefined();
|
|
842
|
-
});
|
|
843
|
-
it("returns err on git failure", async () => {
|
|
844
|
-
const failCommands = new Map([["gc", "fatal: gc failed"]]);
|
|
845
|
-
const { deps } = createMockDeps({ preInitialized: true, failCommands });
|
|
846
|
-
const manager = createConfigGitManager(deps);
|
|
847
|
-
const result = await manager.gc();
|
|
848
|
-
expect(result.ok).toBe(false);
|
|
849
|
-
if (!result.ok) {
|
|
850
|
-
expect(result.error).toContain("git gc failed");
|
|
851
|
-
}
|
|
852
|
-
});
|
|
853
|
-
});
|
|
854
|
-
// -----------------------------------------------------------------------
|
|
855
|
-
// Tests: CORE-08 — encodeCommitMessage sanitization
|
|
856
|
-
// -----------------------------------------------------------------------
|
|
857
|
-
describe("encodeCommitMessage (CORE-08)", () => {
|
|
858
|
-
it("strips newlines from metadata fields", () => {
|
|
859
|
-
const message = encodeCommitMessage({
|
|
860
|
-
section: "agent",
|
|
861
|
-
summary: "line1\nline2\rline3",
|
|
862
|
-
});
|
|
863
|
-
expect(message).not.toContain("\nline2");
|
|
864
|
-
expect(message).toContain("line1 line2 line3");
|
|
865
|
-
});
|
|
866
|
-
it("strips leading [ from metadata fields", () => {
|
|
867
|
-
const message = encodeCommitMessage({
|
|
868
|
-
section: "[injected] tag",
|
|
869
|
-
summary: "normal summary",
|
|
870
|
-
});
|
|
871
|
-
expect(message).toContain("[section] injected] tag");
|
|
872
|
-
expect(message).not.toContain("[section] [injected]");
|
|
873
|
-
});
|
|
874
|
-
it("round-trips clean metadata correctly", () => {
|
|
875
|
-
const original = { section: "gateway", summary: "Updated host", key: "host", agent: "bot-1" };
|
|
876
|
-
const encoded = encodeCommitMessage(original);
|
|
877
|
-
const decoded = parseCommitMessage(encoded);
|
|
878
|
-
expect(decoded.section).toBe("gateway");
|
|
879
|
-
expect(decoded.summary).toBe("Updated host");
|
|
880
|
-
expect(decoded.key).toBe("host");
|
|
881
|
-
expect(decoded.agent).toBe("bot-1");
|
|
882
|
-
});
|
|
883
|
-
it("sanitizes all optional fields", () => {
|
|
884
|
-
const message = encodeCommitMessage({
|
|
885
|
-
section: "test",
|
|
886
|
-
summary: "test",
|
|
887
|
-
key: "line\none",
|
|
888
|
-
agent: "[bad]agent",
|
|
889
|
-
user: "user\r\ninjected",
|
|
890
|
-
traceId: "[[trace",
|
|
891
|
-
});
|
|
892
|
-
expect(message).not.toMatch(/\[key\] line\n/);
|
|
893
|
-
expect(message).toContain("[key] line one");
|
|
894
|
-
expect(message).toContain("[agent] bad]agent");
|
|
895
|
-
expect(message).toContain("[user] user injected");
|
|
896
|
-
expect(message).toContain("[trace] trace");
|
|
897
|
-
});
|
|
898
|
-
});
|
|
899
|
-
describe("squash()", () => {
|
|
900
|
-
it("returns squashedCount: 0 when fewer than 2 old commits", async () => {
|
|
901
|
-
// Use a custom execGit that returns only 1 commit for log --reverse
|
|
902
|
-
let initDone = false;
|
|
903
|
-
const execGit = async (args) => {
|
|
904
|
-
if (args[0] === "status" && !initDone) {
|
|
905
|
-
initDone = true;
|
|
906
|
-
return ok("");
|
|
907
|
-
}
|
|
908
|
-
if (args[0] === "log" && args.includes("--reverse")) {
|
|
909
|
-
return ok("abc1234567890000000000000000000000000000 2026-01-01T00:00:00+00:00\n");
|
|
910
|
-
}
|
|
911
|
-
return ok("");
|
|
912
|
-
};
|
|
913
|
-
const deps = {
|
|
914
|
-
configDir: "/test/config",
|
|
915
|
-
execGit,
|
|
916
|
-
writeFile: async () => ok(undefined),
|
|
917
|
-
};
|
|
918
|
-
const manager = createConfigGitManager(deps);
|
|
919
|
-
const result = await manager.squash("2099-01-01T00:00:00Z");
|
|
920
|
-
expect(result.ok).toBe(true);
|
|
921
|
-
if (result.ok) {
|
|
922
|
-
expect(result.value.squashedCount).toBe(0);
|
|
923
|
-
expect(result.value.newRootSha).toBe("");
|
|
924
|
-
}
|
|
925
|
-
});
|
|
926
|
-
it("squashes old commits and rebases newer ones", async () => {
|
|
927
|
-
let initDone = false;
|
|
928
|
-
const calls = [];
|
|
929
|
-
const execGit = async (args) => {
|
|
930
|
-
calls.push({ args });
|
|
931
|
-
if (args[0] === "status" && !initDone) {
|
|
932
|
-
initDone = true;
|
|
933
|
-
return ok("");
|
|
934
|
-
}
|
|
935
|
-
// Return 5 commits: 3 old (2025), 2 new (2026)
|
|
936
|
-
if (args[0] === "log" && args.includes("--reverse")) {
|
|
937
|
-
return ok([
|
|
938
|
-
"aaa0000000000000000000000000000000000001 2025-01-01T00:00:00+00:00",
|
|
939
|
-
"aaa0000000000000000000000000000000000002 2025-06-01T00:00:00+00:00",
|
|
940
|
-
"aaa0000000000000000000000000000000000003 2025-12-01T00:00:00+00:00",
|
|
941
|
-
"bbb0000000000000000000000000000000000004 2026-02-01T00:00:00+00:00",
|
|
942
|
-
"bbb0000000000000000000000000000000000005 2026-02-15T00:00:00+00:00",
|
|
943
|
-
].join("\n"));
|
|
944
|
-
}
|
|
945
|
-
// rev-parse for tree
|
|
946
|
-
if (args[0] === "rev-parse" && args[1]?.includes("^{tree}")) {
|
|
947
|
-
return ok("treeSha123456");
|
|
948
|
-
}
|
|
949
|
-
// commit-tree
|
|
950
|
-
if (args[0] === "commit-tree") {
|
|
951
|
-
return ok("newRootSha789");
|
|
952
|
-
}
|
|
953
|
-
// symbolic-ref
|
|
954
|
-
if (args[0] === "symbolic-ref") {
|
|
955
|
-
return ok("main");
|
|
956
|
-
}
|
|
957
|
-
// rebase
|
|
958
|
-
if (args[0] === "rebase") {
|
|
959
|
-
return ok("Successfully rebased");
|
|
960
|
-
}
|
|
961
|
-
return ok("");
|
|
962
|
-
};
|
|
963
|
-
const deps = {
|
|
964
|
-
configDir: "/test/config",
|
|
965
|
-
execGit,
|
|
966
|
-
writeFile: async () => ok(undefined),
|
|
967
|
-
};
|
|
968
|
-
const manager = createConfigGitManager(deps);
|
|
969
|
-
// Squash everything older than 2026-01-01
|
|
970
|
-
const result = await manager.squash("2026-01-01T00:00:00Z");
|
|
971
|
-
expect(result.ok).toBe(true);
|
|
972
|
-
if (result.ok) {
|
|
973
|
-
expect(result.value.squashedCount).toBe(3);
|
|
974
|
-
expect(result.value.newRootSha).toBe("newRootSha789");
|
|
975
|
-
}
|
|
976
|
-
// Verify rebase was called with correct args
|
|
977
|
-
const rebaseCall = calls.find((c) => c.args[0] === "rebase");
|
|
978
|
-
expect(rebaseCall).toBeDefined();
|
|
979
|
-
expect(rebaseCall.args).toContain("--onto");
|
|
980
|
-
expect(rebaseCall.args).toContain("newRootSha789");
|
|
981
|
-
});
|
|
982
|
-
it("returns err on invalid date", async () => {
|
|
983
|
-
const { deps } = createMockDeps({ preInitialized: true });
|
|
984
|
-
const manager = createConfigGitManager(deps);
|
|
985
|
-
const result = await manager.squash("not-a-date");
|
|
986
|
-
expect(result.ok).toBe(false);
|
|
987
|
-
if (!result.ok) {
|
|
988
|
-
expect(result.error).toContain("Invalid date");
|
|
989
|
-
}
|
|
990
|
-
});
|
|
991
|
-
it("aborts rebase on failure and returns err", async () => {
|
|
992
|
-
let initDone = false;
|
|
993
|
-
const calls = [];
|
|
994
|
-
const execGit = async (args) => {
|
|
995
|
-
calls.push({ args });
|
|
996
|
-
if (args[0] === "status" && !initDone) {
|
|
997
|
-
initDone = true;
|
|
998
|
-
return ok("");
|
|
999
|
-
}
|
|
1000
|
-
if (args[0] === "log" && args.includes("--reverse")) {
|
|
1001
|
-
return ok([
|
|
1002
|
-
"aaa0000000000000000000000000000000000001 2025-01-01T00:00:00+00:00",
|
|
1003
|
-
"aaa0000000000000000000000000000000000002 2025-06-01T00:00:00+00:00",
|
|
1004
|
-
"bbb0000000000000000000000000000000000003 2026-02-01T00:00:00+00:00",
|
|
1005
|
-
].join("\n"));
|
|
1006
|
-
}
|
|
1007
|
-
if (args[0] === "rev-parse" && args[1]?.includes("^{tree}")) {
|
|
1008
|
-
return ok("treeSha123456");
|
|
1009
|
-
}
|
|
1010
|
-
if (args[0] === "commit-tree") {
|
|
1011
|
-
return ok("newRootSha789");
|
|
1012
|
-
}
|
|
1013
|
-
if (args[0] === "symbolic-ref") {
|
|
1014
|
-
return ok("main");
|
|
1015
|
-
}
|
|
1016
|
-
// Rebase fails
|
|
1017
|
-
if (args[0] === "rebase" && !args.includes("--abort")) {
|
|
1018
|
-
return err("CONFLICT: merge conflict in config.yaml");
|
|
1019
|
-
}
|
|
1020
|
-
// Rebase --abort succeeds
|
|
1021
|
-
if (args[0] === "rebase" && args.includes("--abort")) {
|
|
1022
|
-
return ok("Rebase aborted");
|
|
1023
|
-
}
|
|
1024
|
-
return ok("");
|
|
1025
|
-
};
|
|
1026
|
-
const deps = {
|
|
1027
|
-
configDir: "/test/config",
|
|
1028
|
-
execGit,
|
|
1029
|
-
writeFile: async () => ok(undefined),
|
|
1030
|
-
};
|
|
1031
|
-
const manager = createConfigGitManager(deps);
|
|
1032
|
-
const result = await manager.squash("2026-01-01T00:00:00Z");
|
|
1033
|
-
expect(result.ok).toBe(false);
|
|
1034
|
-
if (!result.ok) {
|
|
1035
|
-
expect(result.error).toContain("Squash rebase failed");
|
|
1036
|
-
}
|
|
1037
|
-
// Verify rebase --abort was called
|
|
1038
|
-
const abortCall = calls.find((c) => c.args[0] === "rebase" && c.args.includes("--abort"));
|
|
1039
|
-
expect(abortCall).toBeDefined();
|
|
1040
|
-
});
|
|
1041
|
-
});
|
|
1042
|
-
});
|