@cvr/repo 1.0.0

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.
@@ -0,0 +1,109 @@
1
+ import { Layer, Ref } from "effect"
2
+
3
+ import type { CacheService } from "../services/cache.js"
4
+ import type { GitService } from "../services/git.js"
5
+ import type { MetadataService } from "../services/metadata.js"
6
+ import type { RegistryService } from "../services/registry.js"
7
+ import {
8
+ createMockCacheService,
9
+ createMockGitService,
10
+ createMockMetadataService,
11
+ createMockRegistryService,
12
+ type MockCacheState,
13
+ type MockGitState,
14
+ type MockMetadataState,
15
+ type MockRegistryState,
16
+ } from "./layers/index.js"
17
+ import { createSequenceRef, type RecordedCall } from "./sequence.js"
18
+
19
+ // ─── Re-exports ────────────────────────────────────────────────────────────────
20
+
21
+ export * from "./layers/index.js"
22
+ export * from "./sequence.js"
23
+ export * from "./run-cli.js"
24
+
25
+ // ─── Test Layer Types ──────────────────────────────────────────────────────────
26
+
27
+ /** All services provided by the test layer */
28
+ export type TestServices = GitService | CacheService | MetadataService | RegistryService
29
+
30
+ // ─── Test Layer Composition ────────────────────────────────────────────────────
31
+
32
+ export interface CreateTestLayerOptions {
33
+ /** Initial git state */
34
+ git?: Partial<MockGitState>
35
+ /** Initial cache state */
36
+ cache?: Partial<MockCacheState>
37
+ /** Initial metadata state */
38
+ metadata?: Partial<MockMetadataState>
39
+ /** Initial registry state */
40
+ registry?: Partial<MockRegistryState>
41
+ }
42
+
43
+ export interface TestLayerResult {
44
+ /** Combined layer providing all test services */
45
+ layer: Layer.Layer<TestServices>
46
+ /** Git mock utilities for state inspection */
47
+ git: ReturnType<typeof createMockGitService>
48
+ /** Cache mock utilities for state inspection */
49
+ cache: ReturnType<typeof createMockCacheService>
50
+ /** Metadata mock utilities for state inspection */
51
+ metadata: ReturnType<typeof createMockMetadataService>
52
+ /** Registry mock utilities for state inspection */
53
+ registry: ReturnType<typeof createMockRegistryService>
54
+ /** Sequence ref for recording all service calls in order */
55
+ sequenceRef: Ref.Ref<RecordedCall[]>
56
+ }
57
+
58
+ /**
59
+ * Creates a complete test layer with all mock services.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const { layer, git, cache, sequenceRef } = createTestLayer({
64
+ * git: { defaultBranch: 'main' },
65
+ * cache: { cacheDir: '/custom/cache' },
66
+ * });
67
+ *
68
+ * const result = yield* someCommand.pipe(Effect.provide(layer));
69
+ *
70
+ * // Verify git state
71
+ * const gitState = yield* git.getState();
72
+ * expect(gitState.clonedRepos.size).toBe(1);
73
+ *
74
+ * // Verify call sequence
75
+ * const calls = yield* Ref.get(sequenceRef);
76
+ * expect(calls[0].service).toBe('registry');
77
+ * ```
78
+ */
79
+ export function createTestLayer(options: CreateTestLayerOptions = {}): TestLayerResult {
80
+ const sequenceRef = createSequenceRef()
81
+
82
+ const git = createMockGitService({
83
+ ...(options.git && { initialState: options.git }),
84
+ sequenceRef,
85
+ })
86
+ const cache = createMockCacheService({
87
+ ...(options.cache && { initialState: options.cache }),
88
+ sequenceRef,
89
+ })
90
+ const metadata = createMockMetadataService({
91
+ ...(options.metadata && { initialState: options.metadata }),
92
+ sequenceRef,
93
+ })
94
+ const registry = createMockRegistryService({
95
+ ...(options.registry && { initialState: options.registry }),
96
+ sequenceRef,
97
+ })
98
+
99
+ const layer = Layer.mergeAll(git.layer, cache.layer, metadata.layer, registry.layer)
100
+
101
+ return {
102
+ layer,
103
+ git,
104
+ cache,
105
+ metadata,
106
+ registry,
107
+ sequenceRef,
108
+ }
109
+ }
@@ -0,0 +1,137 @@
1
+ import { Effect, Layer, Option, Ref } from "effect"
2
+ import { CacheService } from "../../services/cache.js"
3
+ import type { PackageSpec } from "../../types.js"
4
+ import { recordCall, type SequenceRef } from "../sequence.js"
5
+
6
+ // ─── Mock State ───────────────────────────────────────────────────────────────
7
+
8
+ export interface MockCacheState {
9
+ store: Map<string, { content: string; size: number }>
10
+ cacheDir: string
11
+ }
12
+
13
+ export const defaultMockCacheState: MockCacheState = {
14
+ store: new Map(),
15
+ cacheDir: "/tmp/test-repo-cache",
16
+ }
17
+
18
+ // ─── Mock Implementation ──────────────────────────────────────────────────────
19
+
20
+ export interface CreateMockCacheServiceOptions {
21
+ initialState?: Partial<MockCacheState>
22
+ sequenceRef?: SequenceRef
23
+ }
24
+
25
+ export function createMockCacheService(options: CreateMockCacheServiceOptions = {}): {
26
+ layer: Layer.Layer<CacheService>
27
+ stateRef: Ref.Ref<MockCacheState>
28
+ getState: () => Effect.Effect<MockCacheState>
29
+ /** Add a file to the mock cache (for test setup) */
30
+ addFile: (path: string, size: number) => Effect.Effect<void>
31
+ } {
32
+ const initialState = options.initialState ?? {}
33
+ const sequenceRef = options.sequenceRef
34
+
35
+ const state: MockCacheState = {
36
+ ...defaultMockCacheState,
37
+ ...initialState,
38
+ store: new Map(initialState.store ?? []),
39
+ }
40
+ const stateRef = Ref.unsafeMake(state)
41
+
42
+ const record = (method: string, args: unknown, result?: unknown): Effect.Effect<void> =>
43
+ sequenceRef ? recordCall(sequenceRef, { service: "cache", method, args, result }) : Effect.void
44
+
45
+ const getPath = (spec: PackageSpec) =>
46
+ Effect.gen(function* () {
47
+ const s = yield* Ref.get(stateRef)
48
+ const version = Option.getOrElse(spec.version, () => "default")
49
+ let result: string
50
+ switch (spec.registry) {
51
+ case "github":
52
+ result = `${s.cacheDir}/${spec.name}`
53
+ break
54
+ case "npm":
55
+ case "pypi":
56
+ case "crates":
57
+ result = `${s.cacheDir}/${spec.name}/${version}`
58
+ break
59
+ }
60
+ yield* record("getPath", { spec }, result)
61
+ return result
62
+ })
63
+
64
+ const layer = Layer.succeed(
65
+ CacheService,
66
+ CacheService.of({
67
+ cacheDir: state.cacheDir,
68
+
69
+ getPath,
70
+
71
+ exists: (spec) =>
72
+ Effect.gen(function* () {
73
+ const path = yield* getPath(spec)
74
+ const s = yield* Ref.get(stateRef)
75
+ const result = s.store.has(path)
76
+ yield* record("exists", { spec }, result)
77
+ return result
78
+ }),
79
+
80
+ remove: (path) =>
81
+ Effect.gen(function* () {
82
+ yield* record("remove", { path })
83
+ yield* Ref.update(stateRef, (s) => {
84
+ const newStore = new Map(s.store)
85
+ for (const key of newStore.keys()) {
86
+ if (key.startsWith(path)) {
87
+ newStore.delete(key)
88
+ }
89
+ }
90
+ return { ...s, store: newStore }
91
+ })
92
+ }),
93
+
94
+ removeAll: () =>
95
+ Effect.gen(function* () {
96
+ yield* record("removeAll", {})
97
+ yield* Ref.update(stateRef, (s) => ({ ...s, store: new Map() }))
98
+ }),
99
+
100
+ getSize: (path) =>
101
+ Effect.gen(function* () {
102
+ const s = yield* Ref.get(stateRef)
103
+ let total = 0
104
+ for (const [key, value] of s.store.entries()) {
105
+ if (key.startsWith(path)) {
106
+ total += value.size
107
+ }
108
+ }
109
+ yield* record("getSize", { path }, total)
110
+ return total
111
+ }),
112
+
113
+ ensureDir: (path) =>
114
+ Effect.gen(function* () {
115
+ yield* record("ensureDir", { path })
116
+ }),
117
+ })
118
+ )
119
+
120
+ const addFile = (path: string, size: number) =>
121
+ Ref.update(stateRef, (s) => {
122
+ const newStore = new Map(s.store)
123
+ newStore.set(path, { content: "", size })
124
+ return { ...s, store: newStore }
125
+ })
126
+
127
+ return {
128
+ layer,
129
+ stateRef,
130
+ getState: () => Ref.get(stateRef),
131
+ addFile,
132
+ }
133
+ }
134
+
135
+ // ─── Preset Configurations ────────────────────────────────────────────────────
136
+
137
+ export const MockCacheServiceDefault = createMockCacheService()
@@ -0,0 +1,104 @@
1
+ import { Effect, Layer, Ref } from "effect"
2
+ import { GitService } from "../../services/git.js"
3
+ import { recordCall, type SequenceRef } from "../sequence.js"
4
+
5
+ // ─── Mock State ───────────────────────────────────────────────────────────────
6
+
7
+ export interface MockGitState {
8
+ clonedRepos: Map<string, { url: string; ref?: string; depth?: number }>
9
+ updatedPaths: string[]
10
+ defaultBranch: string
11
+ currentRef: string
12
+ }
13
+
14
+ export const defaultMockGitState: MockGitState = {
15
+ clonedRepos: new Map(),
16
+ updatedPaths: [],
17
+ defaultBranch: "main",
18
+ currentRef: "v1.0.0",
19
+ }
20
+
21
+ // ─── Mock Implementation ──────────────────────────────────────────────────────
22
+
23
+ export interface CreateMockGitServiceOptions {
24
+ initialState?: Partial<MockGitState>
25
+ sequenceRef?: SequenceRef
26
+ }
27
+
28
+ export function createMockGitService(options: CreateMockGitServiceOptions = {}): {
29
+ layer: Layer.Layer<GitService>
30
+ stateRef: Ref.Ref<MockGitState>
31
+ getState: () => Effect.Effect<MockGitState>
32
+ } {
33
+ const initialState = options.initialState ?? {}
34
+ const sequenceRef = options.sequenceRef
35
+
36
+ const state: MockGitState = {
37
+ ...defaultMockGitState,
38
+ ...initialState,
39
+ clonedRepos: new Map(initialState.clonedRepos ?? []),
40
+ updatedPaths: [...(initialState.updatedPaths ?? [])],
41
+ }
42
+ const stateRef = Ref.unsafeMake(state)
43
+
44
+ const record = (method: string, args: unknown, result?: unknown): Effect.Effect<void> =>
45
+ sequenceRef ? recordCall(sequenceRef, { service: "git", method, args, result }) : Effect.void
46
+
47
+ const layer = Layer.succeed(
48
+ GitService,
49
+ GitService.of({
50
+ clone: (url, dest, options) =>
51
+ Effect.gen(function* () {
52
+ yield* record("clone", { url, dest, options })
53
+ const entry: { url: string; ref?: string; depth?: number } = { url }
54
+ if (options?.ref) entry.ref = options.ref
55
+ if (options?.depth) entry.depth = options.depth
56
+ yield* Ref.update(stateRef, (s) => ({
57
+ ...s,
58
+ clonedRepos: new Map(s.clonedRepos).set(dest, entry),
59
+ }))
60
+ }),
61
+
62
+ update: (path) =>
63
+ Effect.gen(function* () {
64
+ yield* record("update", { path })
65
+ yield* Ref.update(stateRef, (s) => ({
66
+ ...s,
67
+ updatedPaths: [...s.updatedPaths, path],
68
+ }))
69
+ }),
70
+
71
+ isGitRepo: (path) =>
72
+ Effect.gen(function* () {
73
+ const s = yield* Ref.get(stateRef)
74
+ const result = s.clonedRepos.has(path)
75
+ yield* record("isGitRepo", { path }, result)
76
+ return result
77
+ }),
78
+
79
+ getDefaultBranch: (url) =>
80
+ Effect.gen(function* () {
81
+ const s = yield* Ref.get(stateRef)
82
+ yield* record("getDefaultBranch", { url }, s.defaultBranch)
83
+ return s.defaultBranch
84
+ }),
85
+
86
+ getCurrentRef: (path) =>
87
+ Effect.gen(function* () {
88
+ const s = yield* Ref.get(stateRef)
89
+ yield* record("getCurrentRef", { path }, s.currentRef)
90
+ return s.currentRef
91
+ }),
92
+ })
93
+ )
94
+
95
+ return {
96
+ layer,
97
+ stateRef,
98
+ getState: () => Ref.get(stateRef),
99
+ }
100
+ }
101
+
102
+ // ─── Preset Configurations ────────────────────────────────────────────────────
103
+
104
+ export const MockGitServiceDefault = createMockGitService()
@@ -0,0 +1,33 @@
1
+ // Re-export all mock service factories and types
2
+
3
+ export {
4
+ createMockGitService,
5
+ defaultMockGitState,
6
+ MockGitServiceDefault,
7
+ type CreateMockGitServiceOptions,
8
+ type MockGitState,
9
+ } from "./git.js"
10
+
11
+ export {
12
+ createMockCacheService,
13
+ defaultMockCacheState,
14
+ MockCacheServiceDefault,
15
+ type CreateMockCacheServiceOptions,
16
+ type MockCacheState,
17
+ } from "./cache.js"
18
+
19
+ export {
20
+ createMockMetadataService,
21
+ defaultMockMetadataState,
22
+ MockMetadataServiceDefault,
23
+ type CreateMockMetadataServiceOptions,
24
+ type MockMetadataState,
25
+ } from "./metadata.js"
26
+
27
+ export {
28
+ createMockRegistryService,
29
+ defaultMockRegistryState,
30
+ MockRegistryServiceDefault,
31
+ type CreateMockRegistryServiceOptions,
32
+ type MockRegistryState,
33
+ } from "./registry.js"
@@ -0,0 +1,155 @@
1
+ import { Effect, Layer, Option, Ref } from "effect"
2
+ import { MetadataService } from "../../services/metadata.js"
3
+ import type { MetadataIndex, PackageSpec } from "../../types.js"
4
+ import { recordCall, type SequenceRef } from "../sequence.js"
5
+
6
+ // ─── Mock State ───────────────────────────────────────────────────────────────
7
+
8
+ export interface MockMetadataState {
9
+ index: MetadataIndex
10
+ }
11
+
12
+ export const defaultMockMetadataState: MockMetadataState = {
13
+ index: { version: 1, repos: [] },
14
+ }
15
+
16
+ // ─── Mock Implementation ──────────────────────────────────────────────────────
17
+
18
+ export interface CreateMockMetadataServiceOptions {
19
+ initialState?: Partial<MockMetadataState>
20
+ sequenceRef?: SequenceRef
21
+ }
22
+
23
+ export function createMockMetadataService(options: CreateMockMetadataServiceOptions = {}): {
24
+ layer: Layer.Layer<MetadataService>
25
+ stateRef: Ref.Ref<MockMetadataState>
26
+ getState: () => Effect.Effect<MockMetadataState>
27
+ } {
28
+ const initialState = options.initialState ?? {}
29
+ const sequenceRef = options.sequenceRef
30
+
31
+ const state: MockMetadataState = {
32
+ ...defaultMockMetadataState,
33
+ ...initialState,
34
+ index: {
35
+ version: initialState.index?.version ?? 1,
36
+ repos: [...(initialState.index?.repos ?? [])],
37
+ },
38
+ }
39
+ const stateRef = Ref.unsafeMake(state)
40
+
41
+ const record = (method: string, args: unknown, result?: unknown): Effect.Effect<void> =>
42
+ sequenceRef ? recordCall(sequenceRef, { service: "metadata", method, args, result }) : Effect.void
43
+
44
+ const specMatches = (a: PackageSpec, b: PackageSpec): boolean => {
45
+ if (a.registry !== b.registry || a.name !== b.name) return false
46
+ const aVersion = Option.getOrElse(a.version, () => "")
47
+ const bVersion = Option.getOrElse(b.version, () => "")
48
+ return aVersion === bVersion
49
+ }
50
+
51
+ const layer = Layer.succeed(
52
+ MetadataService,
53
+ MetadataService.of({
54
+ load: () =>
55
+ Effect.gen(function* () {
56
+ const s = yield* Ref.get(stateRef)
57
+ yield* record("load", {}, s.index)
58
+ return s.index
59
+ }),
60
+
61
+ save: (newIndex) =>
62
+ Effect.gen(function* () {
63
+ yield* record("save", { index: newIndex })
64
+ yield* Ref.update(stateRef, (s) => ({ ...s, index: newIndex }))
65
+ }),
66
+
67
+ add: (metadata) =>
68
+ Effect.gen(function* () {
69
+ yield* record("add", { metadata })
70
+ yield* Ref.update(stateRef, (s) => {
71
+ const filtered = s.index.repos.filter((r) => !specMatches(r.spec, metadata.spec))
72
+ return {
73
+ ...s,
74
+ index: { ...s.index, repos: [...filtered, metadata] },
75
+ }
76
+ })
77
+ }),
78
+
79
+ remove: (spec) =>
80
+ Effect.gen(function* () {
81
+ const s = yield* Ref.get(stateRef)
82
+ const originalLength = s.index.repos.length
83
+ const filtered = s.index.repos.filter((r) => !specMatches(r.spec, spec))
84
+ const result = filtered.length < originalLength
85
+ yield* record("remove", { spec }, result)
86
+ yield* Ref.set(stateRef, {
87
+ ...s,
88
+ index: { ...s.index, repos: filtered },
89
+ })
90
+ return result
91
+ }),
92
+
93
+ find: (spec) =>
94
+ Effect.gen(function* () {
95
+ const s = yield* Ref.get(stateRef)
96
+ const result = s.index.repos.find((r) => specMatches(r.spec, spec)) ?? null
97
+ yield* record("find", { spec }, result)
98
+ return result
99
+ }),
100
+
101
+ updateAccessTime: (spec) =>
102
+ Effect.gen(function* () {
103
+ yield* record("updateAccessTime", { spec })
104
+ yield* Ref.update(stateRef, (s) => ({
105
+ ...s,
106
+ index: {
107
+ ...s.index,
108
+ repos: s.index.repos.map((r) => {
109
+ if (specMatches(r.spec, spec)) {
110
+ return { ...r, lastAccessedAt: new Date().toISOString() }
111
+ }
112
+ return r
113
+ }),
114
+ },
115
+ }))
116
+ }),
117
+
118
+ findOlderThan: (days) =>
119
+ Effect.gen(function* () {
120
+ const s = yield* Ref.get(stateRef)
121
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000
122
+ const result = s.index.repos.filter(
123
+ (r) => new Date(r.lastAccessedAt).getTime() < cutoff
124
+ )
125
+ yield* record("findOlderThan", { days }, result)
126
+ return result
127
+ }),
128
+
129
+ findLargerThan: (bytes) =>
130
+ Effect.gen(function* () {
131
+ const s = yield* Ref.get(stateRef)
132
+ const result = s.index.repos.filter((r) => r.sizeBytes > bytes)
133
+ yield* record("findLargerThan", { bytes }, result)
134
+ return result
135
+ }),
136
+
137
+ all: () =>
138
+ Effect.gen(function* () {
139
+ const s = yield* Ref.get(stateRef)
140
+ yield* record("all", {}, s.index.repos)
141
+ return s.index.repos
142
+ }),
143
+ })
144
+ )
145
+
146
+ return {
147
+ layer,
148
+ stateRef,
149
+ getState: () => Ref.get(stateRef),
150
+ }
151
+ }
152
+
153
+ // ─── Preset Configurations ────────────────────────────────────────────────────
154
+
155
+ export const MockMetadataServiceDefault = createMockMetadataService()