engrm 0.1.0 → 0.2.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.
Files changed (98) hide show
  1. package/README.md +214 -73
  2. package/bin/build.mjs +97 -0
  3. package/bin/engrm.mjs +13 -0
  4. package/dist/cli.js +2712 -0
  5. package/dist/hooks/elicitation-result.js +1786 -0
  6. package/dist/hooks/post-tool-use.js +2357 -0
  7. package/dist/hooks/pre-compact.js +1321 -0
  8. package/dist/hooks/sentinel.js +1168 -0
  9. package/dist/hooks/session-start.js +1473 -0
  10. package/dist/hooks/stop.js +1834 -0
  11. package/dist/server.js +16628 -0
  12. package/package.json +29 -4
  13. package/packs/api-best-practices.json +182 -0
  14. package/packs/nextjs-patterns.json +68 -0
  15. package/packs/node-security.json +68 -0
  16. package/packs/python-django.json +68 -0
  17. package/packs/react-gotchas.json +182 -0
  18. package/packs/typescript-patterns.json +67 -0
  19. package/packs/web-security.json +182 -0
  20. package/.mcp.json +0 -9
  21. package/AUTH-DESIGN.md +0 -436
  22. package/BRIEF.md +0 -197
  23. package/CLAUDE.md +0 -44
  24. package/COMPETITIVE.md +0 -174
  25. package/CONTEXT-OPTIMIZATION.md +0 -305
  26. package/INFRASTRUCTURE.md +0 -252
  27. package/MARKET.md +0 -230
  28. package/PLAN.md +0 -278
  29. package/SENTINEL.md +0 -293
  30. package/SERVER-API-PLAN.md +0 -553
  31. package/SPEC.md +0 -843
  32. package/SWOT.md +0 -148
  33. package/SYNC-ARCHITECTURE.md +0 -294
  34. package/VIBE-CODER-STRATEGY.md +0 -250
  35. package/bun.lock +0 -375
  36. package/hooks/post-tool-use.ts +0 -144
  37. package/hooks/session-start.ts +0 -64
  38. package/hooks/stop.ts +0 -131
  39. package/mem-page.html +0 -1305
  40. package/src/capture/dedup.test.ts +0 -103
  41. package/src/capture/dedup.ts +0 -76
  42. package/src/capture/extractor.test.ts +0 -245
  43. package/src/capture/extractor.ts +0 -330
  44. package/src/capture/quality.test.ts +0 -168
  45. package/src/capture/quality.ts +0 -104
  46. package/src/capture/retrospective.test.ts +0 -115
  47. package/src/capture/retrospective.ts +0 -121
  48. package/src/capture/scanner.test.ts +0 -131
  49. package/src/capture/scanner.ts +0 -100
  50. package/src/capture/scrubber.test.ts +0 -144
  51. package/src/capture/scrubber.ts +0 -181
  52. package/src/cli.ts +0 -517
  53. package/src/config.ts +0 -238
  54. package/src/context/inject.test.ts +0 -940
  55. package/src/context/inject.ts +0 -382
  56. package/src/embeddings/backfill.ts +0 -50
  57. package/src/embeddings/embedder.test.ts +0 -76
  58. package/src/embeddings/embedder.ts +0 -139
  59. package/src/lifecycle/aging.test.ts +0 -103
  60. package/src/lifecycle/aging.ts +0 -36
  61. package/src/lifecycle/compaction.test.ts +0 -264
  62. package/src/lifecycle/compaction.ts +0 -190
  63. package/src/lifecycle/purge.test.ts +0 -100
  64. package/src/lifecycle/purge.ts +0 -37
  65. package/src/lifecycle/scheduler.test.ts +0 -120
  66. package/src/lifecycle/scheduler.ts +0 -101
  67. package/src/provisioning/browser-auth.ts +0 -172
  68. package/src/provisioning/provision.test.ts +0 -198
  69. package/src/provisioning/provision.ts +0 -94
  70. package/src/register.test.ts +0 -167
  71. package/src/register.ts +0 -178
  72. package/src/server.ts +0 -436
  73. package/src/storage/migrations.test.ts +0 -244
  74. package/src/storage/migrations.ts +0 -261
  75. package/src/storage/outbox.test.ts +0 -229
  76. package/src/storage/outbox.ts +0 -131
  77. package/src/storage/projects.test.ts +0 -137
  78. package/src/storage/projects.ts +0 -184
  79. package/src/storage/sqlite.test.ts +0 -798
  80. package/src/storage/sqlite.ts +0 -934
  81. package/src/storage/vec.test.ts +0 -198
  82. package/src/sync/auth.test.ts +0 -76
  83. package/src/sync/auth.ts +0 -68
  84. package/src/sync/client.ts +0 -183
  85. package/src/sync/engine.test.ts +0 -94
  86. package/src/sync/engine.ts +0 -127
  87. package/src/sync/pull.test.ts +0 -279
  88. package/src/sync/pull.ts +0 -170
  89. package/src/sync/push.test.ts +0 -117
  90. package/src/sync/push.ts +0 -230
  91. package/src/tools/get.ts +0 -34
  92. package/src/tools/pin.ts +0 -47
  93. package/src/tools/save.test.ts +0 -301
  94. package/src/tools/save.ts +0 -231
  95. package/src/tools/search.test.ts +0 -69
  96. package/src/tools/search.ts +0 -181
  97. package/src/tools/timeline.ts +0 -64
  98. package/tsconfig.json +0 -22
@@ -1,198 +0,0 @@
1
- import { describe, expect, test, beforeEach, afterEach } from "bun:test";
2
- import { mkdtempSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import { MemDatabase } from "./sqlite.js";
6
-
7
- let db: MemDatabase;
8
- let tmpDir: string;
9
-
10
- beforeEach(() => {
11
- tmpDir = mkdtempSync(join(tmpdir(), "engrm-vec-test-"));
12
- db = new MemDatabase(join(tmpDir, "test.db"));
13
- });
14
-
15
- afterEach(() => {
16
- db.close();
17
- rmSync(tmpDir, { recursive: true, force: true });
18
- });
19
-
20
- // All tests conditional on sqlite-vec being available
21
- describe("sqlite-vec integration", () => {
22
- test("vecAvailable is true when extension loads", () => {
23
- expect(db.vecAvailable).toBe(true);
24
- });
25
-
26
- test("vec_observations table exists", () => {
27
- if (!db.vecAvailable) return;
28
- const tables = db.db
29
- .query<{ name: string }, []>(
30
- "SELECT name FROM sqlite_master WHERE type='table' AND name='vec_observations'"
31
- )
32
- .all();
33
- expect(tables.length).toBe(1);
34
- });
35
-
36
- test("vecInsert and searchVec round-trip", () => {
37
- if (!db.vecAvailable) return;
38
-
39
- // Create a project + observation
40
- const project = db.upsertProject({
41
- canonical_id: "test/project",
42
- name: "test",
43
- });
44
-
45
- const obs = db.insertObservation({
46
- project_id: project.id,
47
- type: "bugfix",
48
- title: "Fix auth bug",
49
- quality: 0.8,
50
- user_id: "david",
51
- device_id: "laptop",
52
- });
53
-
54
- // Insert embedding
55
- const vec = new Float32Array(384);
56
- vec[0] = 1.0;
57
- vec[1] = 0.5;
58
- db.vecInsert(obs.id, vec);
59
-
60
- // Search with same vector — should find it
61
- const results = db.searchVec(vec, project.id, ["active"], 5);
62
- expect(results.length).toBe(1);
63
- expect(results[0]!.observation_id).toBe(obs.id);
64
- expect(results[0]!.distance).toBe(0); // exact match
65
- });
66
-
67
- test("searchVec filters by project", () => {
68
- if (!db.vecAvailable) return;
69
-
70
- const proj1 = db.upsertProject({ canonical_id: "p1", name: "p1" });
71
- const proj2 = db.upsertProject({ canonical_id: "p2", name: "p2" });
72
-
73
- const obs1 = db.insertObservation({
74
- project_id: proj1.id,
75
- type: "bugfix",
76
- title: "P1 fix",
77
- quality: 0.8,
78
- user_id: "david",
79
- device_id: "laptop",
80
- });
81
- const obs2 = db.insertObservation({
82
- project_id: proj2.id,
83
- type: "bugfix",
84
- title: "P2 fix",
85
- quality: 0.8,
86
- user_id: "david",
87
- device_id: "laptop",
88
- });
89
-
90
- const vec = new Float32Array(384);
91
- vec[0] = 1.0;
92
- db.vecInsert(obs1.id, vec);
93
- db.vecInsert(obs2.id, vec);
94
-
95
- // Search scoped to project 1
96
- const results = db.searchVec(vec, proj1.id, ["active"], 5);
97
- expect(results.length).toBe(1);
98
- expect(results[0]!.observation_id).toBe(obs1.id);
99
- });
100
-
101
- test("searchVec excludes superseded observations", () => {
102
- if (!db.vecAvailable) return;
103
-
104
- const project = db.upsertProject({ canonical_id: "test/proj", name: "test" });
105
-
106
- const obs1 = db.insertObservation({
107
- project_id: project.id,
108
- type: "decision",
109
- title: "Old decision",
110
- quality: 0.7,
111
- user_id: "david",
112
- device_id: "laptop",
113
- });
114
- const obs2 = db.insertObservation({
115
- project_id: project.id,
116
- type: "decision",
117
- title: "New decision",
118
- quality: 0.9,
119
- user_id: "david",
120
- device_id: "laptop",
121
- });
122
-
123
- const vec = new Float32Array(384);
124
- vec[0] = 1.0;
125
- db.vecInsert(obs1.id, vec);
126
- db.vecInsert(obs2.id, vec);
127
-
128
- // Supersede obs1 with obs2
129
- db.supersedeObservation(obs1.id, obs2.id);
130
-
131
- // Search should only return obs2
132
- const results = db.searchVec(vec, null, ["active"], 5);
133
- expect(results.length).toBe(1);
134
- expect(results[0]!.observation_id).toBe(obs2.id);
135
- });
136
-
137
- test("vecDelete removes embedding", () => {
138
- if (!db.vecAvailable) return;
139
-
140
- const project = db.upsertProject({ canonical_id: "t/p", name: "t" });
141
- const obs = db.insertObservation({
142
- project_id: project.id,
143
- type: "bugfix",
144
- title: "Test",
145
- quality: 0.8,
146
- user_id: "u",
147
- device_id: "d",
148
- });
149
-
150
- const vec = new Float32Array(384);
151
- vec[0] = 1.0;
152
- db.vecInsert(obs.id, vec);
153
- expect(db.searchVec(vec, null, ["active"], 5).length).toBe(1);
154
-
155
- db.vecDelete(obs.id);
156
- expect(db.searchVec(vec, null, ["active"], 5).length).toBe(0);
157
- });
158
-
159
- test("getUnembeddedObservations returns unembedded only", () => {
160
- if (!db.vecAvailable) return;
161
-
162
- const project = db.upsertProject({ canonical_id: "t/p", name: "t" });
163
-
164
- const obs1 = db.insertObservation({
165
- project_id: project.id,
166
- type: "bugfix",
167
- title: "Embedded",
168
- quality: 0.8,
169
- user_id: "u",
170
- device_id: "d",
171
- });
172
- db.insertObservation({
173
- project_id: project.id,
174
- type: "bugfix",
175
- title: "Not embedded",
176
- quality: 0.8,
177
- user_id: "u",
178
- device_id: "d",
179
- });
180
-
181
- // Embed only obs1
182
- const vec = new Float32Array(384);
183
- db.vecInsert(obs1.id, vec);
184
-
185
- const unembedded = db.getUnembeddedObservations(10);
186
- expect(unembedded.length).toBe(1);
187
- expect(unembedded[0]!.title).toBe("Not embedded");
188
-
189
- expect(db.getUnembeddedCount()).toBe(1);
190
- });
191
-
192
- test("searchVec returns empty when vecAvailable but no embeddings", () => {
193
- if (!db.vecAvailable) return;
194
- const vec = new Float32Array(384);
195
- const results = db.searchVec(vec, null, ["active"], 5);
196
- expect(results.length).toBe(0);
197
- });
198
- });
@@ -1,76 +0,0 @@
1
- import { describe, expect, test, afterEach } from "bun:test";
2
- import { getApiKey, getBaseUrl, buildSourceId, parseSourceId } from "./auth.js";
3
- import type { Config } from "../config.js";
4
-
5
- function makeConfig(overrides: Partial<Config> = {}): Config {
6
- return {
7
- candengo_url: overrides.candengo_url ?? "https://candengo.com",
8
- candengo_api_key: overrides.candengo_api_key ?? "cvk_test123",
9
- site_id: overrides.site_id ?? "test-site",
10
- namespace: overrides.namespace ?? "dev-memory",
11
- user_id: overrides.user_id ?? "david",
12
- device_id: overrides.device_id ?? "laptop-abc",
13
- user_email: "",
14
- teams: [],
15
- sync: { enabled: true, interval_seconds: 30, batch_size: 50 },
16
- search: { default_limit: 10, local_boost: 1.2, scope: "all" },
17
- scrubbing: { enabled: true, custom_patterns: [], default_sensitivity: "shared" },
18
- };
19
- }
20
-
21
- afterEach(() => {
22
- delete process.env.ENGRM_TOKEN;
23
- });
24
-
25
- describe("getApiKey", () => {
26
- test("returns env var when set", () => {
27
- process.env.ENGRM_TOKEN = "cvk_from_env";
28
- expect(getApiKey(makeConfig())).toBe("cvk_from_env");
29
- });
30
-
31
- test("falls back to config when env var not set", () => {
32
- expect(getApiKey(makeConfig())).toBe("cvk_test123");
33
- });
34
-
35
- test("returns null when both empty", () => {
36
- expect(getApiKey(makeConfig({ candengo_api_key: "" }))).toBeNull();
37
- });
38
-
39
- test("ignores env var without cvk_ prefix", () => {
40
- process.env.ENGRM_TOKEN = "not_a_valid_key";
41
- expect(getApiKey(makeConfig())).toBe("cvk_test123");
42
- });
43
- });
44
-
45
- describe("getBaseUrl", () => {
46
- test("returns config URL", () => {
47
- expect(getBaseUrl(makeConfig())).toBe("https://candengo.com");
48
- });
49
-
50
- test("returns null for empty URL", () => {
51
- expect(getBaseUrl(makeConfig({ candengo_url: "" }))).toBeNull();
52
- });
53
- });
54
-
55
- describe("buildSourceId", () => {
56
- test("produces correct format", () => {
57
- const config = makeConfig({ user_id: "david", device_id: "laptop-abc" });
58
- expect(buildSourceId(config, 42)).toBe("david-laptop-abc-obs-42");
59
- });
60
- });
61
-
62
- describe("parseSourceId", () => {
63
- test("parses valid source ID", () => {
64
- const result = parseSourceId("david-laptop-abc-obs-42");
65
- expect(result).toEqual({
66
- userId: "david",
67
- deviceId: "laptop-abc",
68
- localId: 42,
69
- });
70
- });
71
-
72
- test("returns null for invalid format", () => {
73
- expect(parseSourceId("invalid")).toBeNull();
74
- expect(parseSourceId("")).toBeNull();
75
- });
76
- });
package/src/sync/auth.ts DELETED
@@ -1,68 +0,0 @@
1
- /**
2
- * Sync authentication utilities.
3
- *
4
- * Resolves API credentials from environment variables or config.
5
- * Environment variable takes precedence for CI/CD use.
6
- */
7
-
8
- import type { Config } from "../config.js";
9
-
10
- /**
11
- * Get API key for Candengo Vector.
12
- * Priority: ENGRM_TOKEN env var → config.candengo_api_key
13
- */
14
- export function getApiKey(config: Config): string | null {
15
- const envKey = process.env.ENGRM_TOKEN;
16
- if (envKey && envKey.startsWith("cvk_")) return envKey;
17
- if (config.candengo_api_key && config.candengo_api_key.length > 0) {
18
- return config.candengo_api_key;
19
- }
20
- return null;
21
- }
22
-
23
- /**
24
- * Get base URL for Candengo Vector API.
25
- */
26
- export function getBaseUrl(config: Config): string | null {
27
- if (config.candengo_url && config.candengo_url.length > 0) {
28
- return config.candengo_url;
29
- }
30
- return null;
31
- }
32
-
33
- /**
34
- * Build a globally unique source ID for a record.
35
- * Format: {user_id}-{device_id}-{type}-{local_id}
36
- * Default type is "obs" for backwards compatibility.
37
- */
38
- export function buildSourceId(config: Config, localId: number, type: string = "obs"): string {
39
- return `${config.user_id}-${config.device_id}-${type}-${localId}`;
40
- }
41
-
42
- /**
43
- * Parse a source ID back into its components.
44
- * Returns null if the format doesn't match.
45
- */
46
- export function parseSourceId(
47
- sourceId: string
48
- ): { userId: string; deviceId: string; localId: number } | null {
49
- // Format: {user_id}-{device_id}-obs-{local_id}
50
- // Split on "-obs-" which is our guaranteed delimiter
51
- const obsIndex = sourceId.lastIndexOf("-obs-");
52
- if (obsIndex === -1) return null;
53
-
54
- const prefix = sourceId.slice(0, obsIndex);
55
- const localIdStr = sourceId.slice(obsIndex + 5); // skip "-obs-"
56
- const localId = parseInt(localIdStr, 10);
57
- if (isNaN(localId)) return null;
58
-
59
- // Split prefix on first "-" to get userId and deviceId
60
- const firstDash = prefix.indexOf("-");
61
- if (firstDash === -1) return null;
62
-
63
- return {
64
- userId: prefix.slice(0, firstDash),
65
- deviceId: prefix.slice(firstDash + 1),
66
- localId,
67
- };
68
- }
@@ -1,183 +0,0 @@
1
- /**
2
- * Candengo Vector REST client.
3
- *
4
- * Wraps the Vector API for ingest, search, and delete operations.
5
- * Uses Bun's built-in fetch — no external HTTP dependencies.
6
- */
7
-
8
- import type { Config } from "../config.js";
9
- import { getApiKey, getBaseUrl } from "./auth.js";
10
-
11
- // --- Types ---
12
-
13
- export interface VectorDocument {
14
- site_id: string;
15
- namespace: string;
16
- source_type: string;
17
- source_id: string;
18
- content: string;
19
- metadata: Record<string, unknown>;
20
- }
21
-
22
- export interface VectorSearchResult {
23
- source_id: string;
24
- content: string;
25
- score: number;
26
- metadata: Record<string, unknown>;
27
- }
28
-
29
- export interface VectorSearchResponse {
30
- results: VectorSearchResult[];
31
- total?: number;
32
- }
33
-
34
- export interface VectorChangeFeedResponse {
35
- changes: VectorSearchResult[];
36
- cursor: string;
37
- has_more: boolean;
38
- }
39
-
40
- // --- Client ---
41
-
42
- export class VectorClient {
43
- private readonly baseUrl: string;
44
- private readonly apiKey: string;
45
- readonly siteId: string;
46
- readonly namespace: string;
47
-
48
- constructor(config: Config) {
49
- const baseUrl = getBaseUrl(config);
50
- const apiKey = getApiKey(config);
51
-
52
- if (!baseUrl || !apiKey) {
53
- throw new Error(
54
- "VectorClient requires candengo_url and candengo_api_key"
55
- );
56
- }
57
-
58
- this.baseUrl = baseUrl.replace(/\/$/, ""); // strip trailing slash
59
- this.apiKey = apiKey;
60
- this.siteId = config.site_id;
61
- this.namespace = config.namespace;
62
- }
63
-
64
- /**
65
- * Check if the client has valid configuration.
66
- */
67
- static isConfigured(config: Config): boolean {
68
- return getApiKey(config) !== null && getBaseUrl(config) !== null;
69
- }
70
-
71
- /**
72
- * Ingest a single document.
73
- */
74
- async ingest(doc: VectorDocument): Promise<void> {
75
- await this.request("POST", "/v1/ingest", doc);
76
- }
77
-
78
- /**
79
- * Batch ingest multiple documents.
80
- */
81
- async batchIngest(docs: VectorDocument[]): Promise<void> {
82
- if (docs.length === 0) return;
83
- await this.request("POST", "/v1/ingest/batch", { documents: docs });
84
- }
85
-
86
- /**
87
- * Semantic search with optional metadata filters.
88
- */
89
- async search(
90
- query: string,
91
- metadataFilter?: Record<string, string>,
92
- limit: number = 10
93
- ): Promise<VectorSearchResponse> {
94
- const body: Record<string, unknown> = { query: query, limit: limit };
95
- if (metadataFilter) {
96
- body.metadata_filter = metadataFilter;
97
- }
98
- return this.request("POST", "/v1/search", body);
99
- }
100
-
101
- /**
102
- * Delete documents by source IDs.
103
- */
104
- async deleteBySourceIds(sourceIds: string[]): Promise<void> {
105
- if (sourceIds.length === 0) return;
106
- await this.request("POST", "/v1/documents/delete", {
107
- source_ids: sourceIds,
108
- });
109
- }
110
-
111
- /**
112
- * Pull changes from the sync change feed.
113
- */
114
- async pullChanges(
115
- cursor?: string,
116
- limit: number = 50
117
- ): Promise<VectorChangeFeedResponse> {
118
- const params = new URLSearchParams();
119
- if (cursor) params.set("cursor", cursor);
120
- params.set("namespace", this.namespace);
121
- params.set("limit", String(limit));
122
- return this.request("GET", `/v1/sync/changes?${params.toString()}`);
123
- }
124
-
125
- /**
126
- * Health check.
127
- */
128
- async health(): Promise<boolean> {
129
- try {
130
- await this.request("GET", "/health");
131
- return true;
132
- } catch {
133
- return false;
134
- }
135
- }
136
-
137
- // --- Internal ---
138
-
139
- private async request<T>(
140
- method: string,
141
- path: string,
142
- body?: unknown
143
- ): Promise<T> {
144
- const url = `${this.baseUrl}${path}`;
145
- const headers: Record<string, string> = {
146
- Authorization: `Bearer ${this.apiKey}`,
147
- "Content-Type": "application/json",
148
- };
149
-
150
- const init: RequestInit = { method, headers };
151
- if (body && method !== "GET") {
152
- init.body = JSON.stringify(body);
153
- }
154
-
155
- const response = await fetch(url, init);
156
-
157
- if (!response.ok) {
158
- const text = await response.text().catch(() => "");
159
- throw new VectorApiError(response.status, text, path);
160
- }
161
-
162
- // Some endpoints (DELETE, ingest) may return 204 with no body
163
- if (
164
- response.status === 204 ||
165
- response.headers.get("content-length") === "0"
166
- ) {
167
- return undefined as T;
168
- }
169
-
170
- return response.json() as Promise<T>;
171
- }
172
- }
173
-
174
- export class VectorApiError extends Error {
175
- constructor(
176
- readonly status: number,
177
- readonly body: string,
178
- readonly path: string
179
- ) {
180
- super(`Vector API error ${status} on ${path}: ${body}`);
181
- this.name = "VectorApiError";
182
- }
183
- }
@@ -1,94 +0,0 @@
1
- import { describe, expect, test, beforeEach, afterEach } from "bun:test";
2
- import { mkdtempSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import { MemDatabase } from "../storage/sqlite.js";
6
- import type { Config } from "../config.js";
7
- import { SyncEngine } from "./engine.js";
8
-
9
- let db: MemDatabase;
10
- let tmpDir: string;
11
-
12
- function makeConfig(overrides: Partial<Config> = {}): Config {
13
- return {
14
- candengo_url: overrides.candengo_url ?? "",
15
- candengo_api_key: overrides.candengo_api_key ?? "",
16
- site_id: "test-site",
17
- namespace: "dev-memory",
18
- user_id: "david",
19
- device_id: "laptop-abc",
20
- user_email: "",
21
- teams: [],
22
- sync: { enabled: true, interval_seconds: 30, batch_size: 50 },
23
- search: { default_limit: 10, local_boost: 1.2, scope: "all" },
24
- scrubbing: { enabled: true, custom_patterns: [], default_sensitivity: "shared" },
25
- };
26
- }
27
-
28
- beforeEach(() => {
29
- tmpDir = mkdtempSync(join(tmpdir(), "candengo-engine-test-"));
30
- db = new MemDatabase(join(tmpDir, "test.db"));
31
- });
32
-
33
- afterEach(() => {
34
- db.close();
35
- rmSync(tmpDir, { recursive: true, force: true });
36
- });
37
-
38
- describe("SyncEngine", () => {
39
- test("is not configured when no API key", () => {
40
- const engine = new SyncEngine(db, makeConfig());
41
- expect(engine.isConfigured()).toBe(false);
42
- });
43
-
44
- test("is configured when API key and URL provided", () => {
45
- const engine = new SyncEngine(
46
- db,
47
- makeConfig({
48
- candengo_url: "https://candengo.com",
49
- candengo_api_key: "cvk_test123",
50
- })
51
- );
52
- expect(engine.isConfigured()).toBe(true);
53
- });
54
-
55
- test("start is no-op when not configured", () => {
56
- const engine = new SyncEngine(db, makeConfig());
57
- engine.start();
58
- expect(engine.isRunning()).toBe(false);
59
- engine.stop(); // should not throw
60
- });
61
-
62
- test("start and stop work cleanly when configured", () => {
63
- const engine = new SyncEngine(
64
- db,
65
- makeConfig({
66
- candengo_url: "https://candengo.com",
67
- candengo_api_key: "cvk_test123",
68
- })
69
- );
70
- engine.start();
71
- expect(engine.isRunning()).toBe(true);
72
- engine.stop();
73
- expect(engine.isRunning()).toBe(false);
74
- });
75
-
76
- test("stop is idempotent", () => {
77
- const engine = new SyncEngine(db, makeConfig());
78
- engine.stop();
79
- engine.stop();
80
- // Should not throw
81
- });
82
-
83
- test("start with sync disabled is no-op", () => {
84
- const config = makeConfig({
85
- candengo_url: "https://candengo.com",
86
- candengo_api_key: "cvk_test123",
87
- });
88
- config.sync.enabled = false;
89
- const engine = new SyncEngine(db, config);
90
- engine.start();
91
- expect(engine.isRunning()).toBe(false);
92
- engine.stop();
93
- });
94
- });