@inetafrica/open-claudia 2.15.0 → 3.0.1

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 (152) hide show
  1. package/.env.example +30 -4
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +87 -66
  4. package/bin/agent.js +44 -18
  5. package/bin/cli.js +30 -28
  6. package/bin/dream.js +5 -11
  7. package/bin/loopback-client.js +29 -5
  8. package/bin/schedule.js +19 -5
  9. package/bin/tool.js +23 -5
  10. package/bot-agent.js +5 -2350
  11. package/bot.js +81 -3
  12. package/channels/telegram/adapter.js +65 -5
  13. package/channels/voice/adapter.js +1 -0
  14. package/channels/voice/manage.js +43 -5
  15. package/config-dir.js +3 -11
  16. package/core/actions.js +155 -40
  17. package/core/approvals.js +136 -29
  18. package/core/auth-flow.js +103 -19
  19. package/core/config-dir.js +19 -0
  20. package/core/config.js +115 -69
  21. package/core/doctor.js +111 -57
  22. package/core/dream.js +219 -62
  23. package/core/enforcer.js +0 -0
  24. package/core/entities.js +2 -1
  25. package/core/fsutil.js +21 -4
  26. package/core/handlers.js +542 -224
  27. package/core/ideas.js +2 -1
  28. package/core/jobs.js +589 -72
  29. package/core/lessons.js +3 -2
  30. package/core/loopback.js +42 -6
  31. package/core/memory-mutation-queue.js +241 -0
  32. package/core/migration-backup.js +1060 -0
  33. package/core/pack-review.js +295 -88
  34. package/core/packs.js +4 -3
  35. package/core/persona.js +2 -1
  36. package/core/process-tree.js +19 -2
  37. package/core/provider-migration.js +39 -0
  38. package/core/provider-status.js +80 -0
  39. package/core/providers/claude-events.js +121 -0
  40. package/core/providers/claude-hook.js +114 -0
  41. package/core/providers/claude.js +296 -0
  42. package/core/providers/codex-events.js +125 -0
  43. package/core/providers/codex-hook.js +280 -0
  44. package/core/providers/codex.js +286 -0
  45. package/core/providers/contract.js +153 -0
  46. package/core/providers/env.js +148 -0
  47. package/core/providers/events.js +171 -0
  48. package/core/providers/hook-command.js +22 -0
  49. package/core/providers/index.js +240 -0
  50. package/core/providers/utility-policy.js +269 -0
  51. package/core/recall/classic.js +2 -2
  52. package/core/recall/discoverer.js +71 -22
  53. package/core/recall/metrics.js +27 -1
  54. package/core/recall/tuning.js +4 -1
  55. package/core/recall/warm-walker.js +151 -108
  56. package/core/recall-filter.js +86 -61
  57. package/core/router.js +79 -7
  58. package/core/run-context.js +185 -0
  59. package/core/runner.js +1562 -1286
  60. package/core/scheduler.js +515 -210
  61. package/core/side-chat.js +346 -0
  62. package/core/single-instance.js +46 -0
  63. package/core/state.js +1084 -97
  64. package/core/subagent.js +72 -98
  65. package/core/system-prompt.js +462 -279
  66. package/core/tool-guard.js +73 -39
  67. package/core/tools.js +22 -5
  68. package/core/transcripts.js +30 -1
  69. package/core/usage-log.js +19 -4
  70. package/core/utility-agent.js +654 -0
  71. package/core/web-auth.js +29 -13
  72. package/docs/CHANNEL_DESIGN.md +181 -0
  73. package/docs/MULTI_CHANNEL_PLAN.md +254 -0
  74. package/docs/PROVIDER_MIGRATION.md +67 -0
  75. package/health.js +50 -39
  76. package/package.json +48 -4
  77. package/setup.js +198 -38
  78. package/soul.md +39 -0
  79. package/test-ability-extraction.js +5 -0
  80. package/test-approval-async.js +63 -4
  81. package/test-cursor-removal.js +337 -0
  82. package/test-enforcer.js +70 -27
  83. package/test-fixtures/current-provider-parity.json +132 -0
  84. package/test-fixtures/fake-agent-cli.js +509 -0
  85. package/test-fixtures/migrations/claude-only/jobs.json +3 -0
  86. package/test-fixtures/migrations/claude-only/sessions.json +5 -0
  87. package/test-fixtures/migrations/claude-only/state.json +5 -0
  88. package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
  89. package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
  90. package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
  91. package/test-fixtures/migrations/cursor-selected/state.json +6 -0
  92. package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
  93. package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
  94. package/test-fixtures/migrations/dual-provider/state.json +10 -0
  95. package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
  96. package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
  97. package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
  98. package/test-fixtures/migrations/malformed-partial/state.json +1 -0
  99. package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
  100. package/test-fixtures/migrations/missing-project/jobs.json +3 -0
  101. package/test-fixtures/migrations/missing-project/sessions.json +3 -0
  102. package/test-fixtures/migrations/missing-project/state.json +4 -0
  103. package/test-fixtures/migrations/multi-user/jobs.json +4 -0
  104. package/test-fixtures/migrations/multi-user/sessions.json +4 -0
  105. package/test-fixtures/migrations/multi-user/state.json +6 -0
  106. package/test-fixtures/provider-event-samples.json +17 -0
  107. package/test-learning-e2e.js +5 -0
  108. package/test-memory-mutation-queue.js +191 -0
  109. package/test-provider-boot-matrix.js +399 -0
  110. package/test-provider-bootstrap.js +158 -0
  111. package/test-provider-capabilities.js +232 -0
  112. package/test-provider-claude.js +206 -0
  113. package/test-provider-codex.js +228 -0
  114. package/test-provider-compaction.js +371 -0
  115. package/test-provider-core-prompt.js +264 -0
  116. package/test-provider-coupling-audit.js +90 -0
  117. package/test-provider-dream.js +312 -0
  118. package/test-provider-enforcer.js +252 -0
  119. package/test-provider-env-isolation.js +150 -0
  120. package/test-provider-events.js +171 -0
  121. package/test-provider-fixture.js +332 -0
  122. package/test-provider-gateway.js +508 -0
  123. package/test-provider-language.js +89 -0
  124. package/test-provider-migration-backup.js +424 -0
  125. package/test-provider-pack-review.js +436 -0
  126. package/test-provider-parity-e2e.js +537 -0
  127. package/test-provider-prompt-parity.js +89 -0
  128. package/test-provider-recall.js +271 -0
  129. package/test-provider-registry.js +251 -0
  130. package/test-provider-resume-parity.js +41 -0
  131. package/test-provider-run-lifecycle.js +349 -0
  132. package/test-provider-runner.js +271 -0
  133. package/test-provider-scheduler.js +689 -0
  134. package/test-provider-session-commands.js +185 -0
  135. package/test-provider-session-history.js +205 -0
  136. package/test-provider-side-chat.js +337 -0
  137. package/test-provider-state-migration.js +316 -0
  138. package/test-provider-stream-decoder.js +69 -0
  139. package/test-provider-subagent.js +228 -0
  140. package/test-provider-tool-hooks.js +360 -0
  141. package/test-recall-discoverer.js +1 -0
  142. package/test-recall-engine.js +3 -3
  143. package/test-recall-evolution.js +18 -0
  144. package/test-runner-watchdog-static.js +16 -8
  145. package/test-single-runtime.js +35 -0
  146. package/test-telegram-poll-recovery.js +93 -0
  147. package/test-tool-guard.js +56 -0
  148. package/test-tools.js +19 -0
  149. package/test-unified-identity.js +3 -1
  150. package/test-usage-accounting.js +92 -20
  151. package/test-utility-provider-policy.js +486 -0
  152. package/web.js +130 -35
@@ -0,0 +1,424 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const assert = require("assert");
6
+ const fs = require("fs");
7
+ const os = require("os");
8
+ const path = require("path");
9
+ const {
10
+ MIGRATION_ID,
11
+ REQUIRED_ACTIVATION_COMPONENTS,
12
+ defaultMigrationSources,
13
+ ensureMigrationSnapshot,
14
+ markMigrationComponentActivated,
15
+ markMigrationComponentPrepared,
16
+ readMigrationJournal,
17
+ rollbackMigrationSnapshot,
18
+ sha256,
19
+ validateMigrationSnapshot,
20
+ } = require("./core/migration-backup");
21
+
22
+ const fixturesRoot = path.join(__dirname, "test-fixtures", "migrations");
23
+ const fixtureNames = [
24
+ "claude-only",
25
+ "dual-provider",
26
+ "cursor-selected",
27
+ "multi-user",
28
+ "malformed-partial",
29
+ "missing-project",
30
+ ];
31
+ const fixedDate = new Date("2026-07-10T08:00:00.000Z");
32
+
33
+ function tempRoot(label) {
34
+ return fs.mkdtempSync(path.join(os.tmpdir(), `open-claudia-migration-${label}-`));
35
+ }
36
+
37
+ function installFixture(name, root) {
38
+ const configDir = path.join(root, "config");
39
+ fs.mkdirSync(configDir, { recursive: true });
40
+ fs.cpSync(path.join(fixturesRoot, name), configDir, { recursive: true });
41
+ return configDir;
42
+ }
43
+
44
+ function options(configDir, extra = {}) {
45
+ let sequence = 0;
46
+ return {
47
+ configDir,
48
+ sources: defaultMigrationSources({ configDir }),
49
+ now: () => fixedDate,
50
+ idFactory: () => `fixture-${String(++sequence).padStart(2, "0")}`,
51
+ ...extra,
52
+ };
53
+ }
54
+
55
+ function rawSources(sources) {
56
+ const result = new Map();
57
+ for (const source of sources) {
58
+ result.set(source.path, fs.existsSync(source.sourcePath) ? fs.readFileSync(source.sourcePath) : null);
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function assertSourcesEqual(sources, expected, label) {
64
+ for (const source of sources) {
65
+ const before = expected.get(source.path);
66
+ const exists = fs.existsSync(source.sourcePath);
67
+ assert.strictEqual(exists, before !== null, `${label}: presence changed for ${source.path}`);
68
+ if (before !== null) assert.deepStrictEqual(fs.readFileSync(source.sourcePath), before, `${label}: bytes changed for ${source.path}`);
69
+ }
70
+ }
71
+
72
+ function manifestFile(snapshotDir) {
73
+ return path.join(snapshotDir, "manifest.json");
74
+ }
75
+
76
+ function backupFile(snapshotDir, logicalPath) {
77
+ return path.join(snapshotDir, "originals", ...logicalPath.split("/"));
78
+ }
79
+
80
+ function assertProtectedModes(snapshotDir, manifest) {
81
+ if (process.platform === "win32") return;
82
+ assert.strictEqual(fs.statSync(snapshotDir).mode & 0o777, 0o700);
83
+ assert.strictEqual(fs.statSync(path.join(snapshotDir, "originals")).mode & 0o777, 0o700);
84
+ assert.strictEqual(fs.statSync(manifestFile(snapshotDir)).mode & 0o777, 0o600);
85
+ for (const entry of manifest.files.filter((item) => item.present)) {
86
+ assert.strictEqual(fs.statSync(backupFile(snapshotDir, entry.path)).mode & 0o777, 0o600);
87
+ }
88
+ }
89
+
90
+ function assertManifest(snapshot, sources, before, fixtureName) {
91
+ const { manifest, snapshotDir } = snapshot;
92
+ assert.strictEqual(manifest.migrationId, MIGRATION_ID);
93
+ assert.strictEqual(manifest.formatVersion, 1);
94
+ assert.strictEqual(manifest.createdAt, fixedDate.toISOString());
95
+ assert.strictEqual(manifest.files.length, sources.length);
96
+ assert.deepStrictEqual(manifest.files.map((entry) => entry.path), [...manifest.files.map((entry) => entry.path)].sort());
97
+ const manifestText = fs.readFileSync(manifestFile(snapshotDir), "utf8");
98
+ assert.ok(!manifestText.includes(path.dirname(snapshotDir)), "manifest must not expose absolute installation paths");
99
+ assert.ok(!manifestText.includes("SECRET_SENTINEL_DO_NOT_MANIFEST"), "manifest must not echo source contents");
100
+ for (const entry of manifest.files) {
101
+ assert.ok(!path.isAbsolute(entry.path));
102
+ assert.ok(!entry.path.includes(".."));
103
+ assert.strictEqual(typeof entry.schemaGuess, "string");
104
+ assert.ok(["archived", "unresolved", "missing"].includes(entry.resolution));
105
+ const raw = before.get(entry.path);
106
+ assert.strictEqual(entry.present, raw !== null, `${fixtureName}: manifest presence for ${entry.path}`);
107
+ if (raw === null) {
108
+ assert.strictEqual(entry.size, null);
109
+ assert.strictEqual(entry.sha256, null);
110
+ continue;
111
+ }
112
+ assert.strictEqual(entry.size, raw.length);
113
+ assert.strictEqual(entry.sha256, sha256(raw));
114
+ assert.deepStrictEqual(fs.readFileSync(backupFile(snapshotDir, entry.path)), raw);
115
+ }
116
+ assertProtectedModes(snapshotDir, manifest);
117
+ }
118
+
119
+ function fixtureMatrix() {
120
+ for (const fixtureName of fixtureNames) {
121
+ const root = tempRoot(fixtureName);
122
+ try {
123
+ const configDir = installFixture(fixtureName, root);
124
+ const opts = options(configDir);
125
+ const before = rawSources(opts.sources);
126
+ const snapshot = ensureMigrationSnapshot(opts);
127
+ assert.strictEqual(snapshot.reused, false);
128
+ assert.strictEqual(snapshot.journal.stage, "validated");
129
+ assertManifest(snapshot, opts.sources, before, fixtureName);
130
+ const validated = validateMigrationSnapshot(snapshot.snapshotDir, { sources: opts.sources, verifySources: true });
131
+ assert.strictEqual(validated.ok, true);
132
+ assertSourcesEqual(opts.sources, before, `${fixtureName} snapshot`);
133
+
134
+ const rerun = ensureMigrationSnapshot(opts);
135
+ assert.strictEqual(rerun.reused, true, `${fixtureName}: unchanged rerun reuses snapshot`);
136
+ assert.strictEqual(rerun.snapshotDir, snapshot.snapshotDir);
137
+ assert.strictEqual(readMigrationJournal(configDir).stage, "validated");
138
+
139
+ const entries = new Map(snapshot.manifest.files.map((entry) => [entry.path, entry]));
140
+ if (fixtureName === "cursor-selected") {
141
+ assert.strictEqual(entries.get("state.json").resolution, "unresolved");
142
+ assert.match(entries.get("state.json").schemaGuess, /cursor/);
143
+ assert.strictEqual(entries.get("jobs.json").resolution, "unresolved");
144
+ }
145
+ if (fixtureName === "malformed-partial") {
146
+ assert.strictEqual(entries.get("state.json").schemaGuess, "malformed-json");
147
+ assert.strictEqual(entries.get("state.json").resolution, "unresolved");
148
+ assert.strictEqual(entries.get("state.json.bak").present, true);
149
+ assert.strictEqual(entries.get("sessions.json.bak").present, true);
150
+ }
151
+ if (fixtureName === "missing-project") {
152
+ assert.strictEqual(entries.get("state.json").resolution, "unresolved");
153
+ assert.strictEqual(entries.get("jobs.json").resolution, "unresolved");
154
+ }
155
+ } finally {
156
+ fs.rmSync(root, { recursive: true, force: true });
157
+ }
158
+ }
159
+ }
160
+
161
+ function interruptionProbe() {
162
+ const points = [
163
+ "after-copy:state.json",
164
+ "after-manifest",
165
+ "after-prepared",
166
+ "after-validated",
167
+ ];
168
+ for (const point of points) {
169
+ const root = tempRoot(`interrupt-${point.replace(/[^a-z]/gi, "-")}`);
170
+ try {
171
+ const configDir = installFixture("cursor-selected", root);
172
+ const base = options(configDir);
173
+ const before = rawSources(base.sources);
174
+ assert.throws(() => ensureMigrationSnapshot({
175
+ ...base,
176
+ faultInjector(current) {
177
+ if (current === point) {
178
+ const error = new Error(`fixture interruption at ${point}`);
179
+ error.code = "FIXTURE_INTERRUPTION";
180
+ throw error;
181
+ }
182
+ },
183
+ }), (error) => error.code === "FIXTURE_INTERRUPTION");
184
+ assertSourcesEqual(base.sources, before, point);
185
+ const recovered = ensureMigrationSnapshot(base);
186
+ assert.strictEqual(recovered.journal.stage, "validated", `${point}: rerun recovers idempotently`);
187
+ assertSourcesEqual(base.sources, before, `${point} recovery`);
188
+ } finally {
189
+ fs.rmSync(root, { recursive: true, force: true });
190
+ }
191
+ }
192
+ }
193
+
194
+ function corruptionAndLockProbe() {
195
+ const root = tempRoot("corrupt");
196
+ try {
197
+ const configDir = installFixture("claude-only", root);
198
+ const opts = options(configDir);
199
+ const before = rawSources(opts.sources);
200
+
201
+ assert.throws(() => ensureMigrationSnapshot({
202
+ ...opts,
203
+ faultInjector(point, context) {
204
+ if (point === "before-copy-verify:state.json") fs.appendFileSync(context.backupPath, "corrupt");
205
+ },
206
+ }), (error) => error.code === "BACKUP_VERIFICATION_FAILED");
207
+ assertSourcesEqual(opts.sources, before, "copy verification failure");
208
+
209
+ const snapshot = ensureMigrationSnapshot(opts);
210
+ const stateBackup = backupFile(snapshot.snapshotDir, "state.json");
211
+ fs.appendFileSync(stateBackup, "corrupt");
212
+ assert.throws(
213
+ () => validateMigrationSnapshot(snapshot.snapshotDir),
214
+ (error) => error.code === "BACKUP_CHECKSUM_MISMATCH",
215
+ );
216
+ assert.throws(
217
+ () => markMigrationComponentActivated(snapshot.snapshotDir, "state", { configDir, sources: opts.sources }),
218
+ (error) => error.code === "BACKUP_CHECKSUM_MISMATCH",
219
+ );
220
+ assertSourcesEqual(opts.sources, before, "corrupt backup detection");
221
+
222
+ fs.writeFileSync(path.join(configDir, "migration-backups", MIGRATION_ID, ".lock"), "held", { mode: 0o600 });
223
+ assert.throws(() => ensureMigrationSnapshot(opts), (error) => error.code === "MIGRATION_LOCKED");
224
+ } finally {
225
+ fs.rmSync(root, { recursive: true, force: true });
226
+ }
227
+
228
+ const symlinkRoot = tempRoot("symlink");
229
+ try {
230
+ const configDir = path.join(symlinkRoot, "config");
231
+ fs.mkdirSync(configDir, { recursive: true });
232
+ const outside = path.join(symlinkRoot, "outside.json");
233
+ fs.writeFileSync(outside, "{}\n");
234
+ fs.symlinkSync(outside, path.join(configDir, "state.json"));
235
+ assert.throws(() => ensureMigrationSnapshot(options(configDir)), (error) => error.code === "UNSAFE_MIGRATION_SOURCE");
236
+ } finally {
237
+ fs.rmSync(symlinkRoot, { recursive: true, force: true });
238
+ }
239
+ }
240
+
241
+ function activationProbe() {
242
+ const root = tempRoot("activate");
243
+ try {
244
+ const configDir = installFixture("dual-provider", root);
245
+ const opts = options(configDir);
246
+ const snapshot = ensureMigrationSnapshot(opts);
247
+ const migrated = {
248
+ state: { schemaVersion: 3, users: {} },
249
+ sessions: { schemaVersion: 3, users: {} },
250
+ jobs: {
251
+ schemaVersion: 2,
252
+ jobs: [],
253
+ archived: [],
254
+ legacyCronMigration: {
255
+ path: "crons.json",
256
+ present: false,
257
+ size: null,
258
+ sha256: null,
259
+ status: "missing",
260
+ recordCount: 0,
261
+ recordHashes: [],
262
+ },
263
+ },
264
+ };
265
+ for (const component of REQUIRED_ACTIVATION_COMPONENTS) {
266
+ const prepared = markMigrationComponentPrepared(snapshot.snapshotDir, component, { configDir, sources: opts.sources });
267
+ assert.ok(prepared.pendingComponents[component]);
268
+ assert.strictEqual(prepared.components[component], null);
269
+ fs.writeFileSync(path.join(configDir, `${component}.json`), `${JSON.stringify(migrated[component], null, 2)}\n`);
270
+ assert.throws(() => markMigrationComponentActivated(snapshot.snapshotDir, component, {
271
+ configDir,
272
+ sources: opts.sources,
273
+ faultInjector(point) {
274
+ if (point === `after-activation:${component}`) {
275
+ const error = new Error(`activation interrupted: ${component}`);
276
+ error.code = "FIXTURE_INTERRUPTION";
277
+ throw error;
278
+ }
279
+ },
280
+ }), (error) => error.code === "FIXTURE_INTERRUPTION");
281
+ const journal = markMigrationComponentActivated(snapshot.snapshotDir, component, { configDir, sources: opts.sources });
282
+ assert.ok(!journal.pendingComponents[component]);
283
+ const isLast = component === REQUIRED_ACTIVATION_COMPONENTS.at(-1);
284
+ assert.strictEqual(journal.stage, isLast ? "activated" : "validated");
285
+ }
286
+ const rerun = markMigrationComponentActivated(snapshot.snapshotDir, "jobs", { configDir, sources: opts.sources });
287
+ assert.strictEqual(rerun.stage, "activated");
288
+ assert.strictEqual(Object.keys(rerun.components).length, REQUIRED_ACTIVATION_COMPONENTS.length);
289
+
290
+ fs.appendFileSync(path.join(configDir, "state.json"), "\n");
291
+ assert.throws(
292
+ () => markMigrationComponentActivated(snapshot.snapshotDir, "state", { configDir, sources: opts.sources, requireSourceMatch: true }),
293
+ (error) => error.code === "MIGRATION_SOURCE_CHANGED",
294
+ );
295
+ } finally {
296
+ fs.rmSync(root, { recursive: true, force: true });
297
+ }
298
+ }
299
+
300
+ function rawByteProbe() {
301
+ const root = tempRoot("raw-bytes");
302
+ try {
303
+ const configDir = path.join(root, "config");
304
+ fs.mkdirSync(configDir, { recursive: true });
305
+ fs.writeFileSync(path.join(configDir, "state.json"), Buffer.from('{\r\n "settings": {"backend": "claude"}\r\n}\r\n', "utf8"));
306
+ fs.writeFileSync(path.join(configDir, "sessions.json"), Buffer.alloc(0));
307
+ fs.writeFileSync(path.join(configDir, "jobs.json"), Buffer.from('[{"id":"raw-job"}]', "utf8"));
308
+ const opts = options(configDir);
309
+ const before = rawSources(opts.sources);
310
+ const snapshot = ensureMigrationSnapshot(opts);
311
+ assertManifest(snapshot, opts.sources, before, "raw bytes");
312
+ fs.writeFileSync(path.join(configDir, "state.json"), "mutated\n");
313
+ fs.writeFileSync(path.join(configDir, "sessions.json"), "mutated\n");
314
+ rollbackMigrationSnapshot(snapshot.snapshotDir, { configDir, sources: opts.sources });
315
+ assertSourcesEqual(opts.sources, before, "raw CRLF/zero-byte rollback");
316
+ } finally {
317
+ fs.rmSync(root, { recursive: true, force: true });
318
+ }
319
+ }
320
+
321
+ function rollbackProbe() {
322
+ const root = tempRoot("rollback");
323
+ try {
324
+ const configDir = installFixture("malformed-partial", root);
325
+ const opts = options(configDir);
326
+ const before = rawSources(opts.sources);
327
+ const snapshot = ensureMigrationSnapshot(opts);
328
+
329
+ for (const source of opts.sources) {
330
+ fs.mkdirSync(path.dirname(source.sourcePath), { recursive: true });
331
+ fs.writeFileSync(source.sourcePath, `mutated:${source.path}\n`);
332
+ }
333
+ let interrupted = false;
334
+ assert.throws(() => rollbackMigrationSnapshot(snapshot.snapshotDir, {
335
+ configDir,
336
+ sources: opts.sources,
337
+ faultInjector(point) {
338
+ if (!interrupted && point === "after-rollback:jobs.json") {
339
+ interrupted = true;
340
+ const error = new Error("rollback interrupted");
341
+ error.code = "FIXTURE_INTERRUPTION";
342
+ throw error;
343
+ }
344
+ },
345
+ }), (error) => error.code === "FIXTURE_INTERRUPTION");
346
+ rollbackMigrationSnapshot(snapshot.snapshotDir, { configDir, sources: opts.sources });
347
+ assertSourcesEqual(opts.sources, before, "rollback retry");
348
+
349
+ // Validation happens before the first restore target is touched.
350
+ for (const source of opts.sources) {
351
+ if (fs.existsSync(source.sourcePath)) fs.writeFileSync(source.sourcePath, `second-mutation:${source.path}\n`);
352
+ }
353
+ const beforeBlockedRollback = rawSources(opts.sources);
354
+ fs.appendFileSync(backupFile(snapshot.snapshotDir, "state.json"), "corrupt");
355
+ assert.throws(
356
+ () => rollbackMigrationSnapshot(snapshot.snapshotDir, { configDir, sources: opts.sources }),
357
+ (error) => error.code === "BACKUP_CHECKSUM_MISMATCH",
358
+ );
359
+ assertSourcesEqual(opts.sources, beforeBlockedRollback, "blocked rollback");
360
+ } finally {
361
+ fs.rmSync(root, { recursive: true, force: true });
362
+ }
363
+ }
364
+
365
+ function sourceChangeProbe() {
366
+ const root = tempRoot("source-change");
367
+ try {
368
+ const configDir = installFixture("claude-only", root);
369
+ const opts = options(configDir);
370
+ assert.throws(() => ensureMigrationSnapshot({
371
+ ...opts,
372
+ faultInjector(point) {
373
+ if (point === "before-source-validation") fs.appendFileSync(path.join(configDir, "state.json"), "\n");
374
+ },
375
+ }), (error) => error.code === "MIGRATION_SOURCE_CHANGED");
376
+ } finally {
377
+ fs.rmSync(root, { recursive: true, force: true });
378
+ }
379
+ }
380
+
381
+ function integrationAndDocsProbe() {
382
+ const bot = fs.readFileSync(path.join(__dirname, "bot.js"), "utf8");
383
+ const scheduler = fs.readFileSync(path.join(__dirname, "core", "scheduler.js"), "utf8");
384
+ const jobs = fs.readFileSync(path.join(__dirname, "core", "jobs.js"), "utf8");
385
+ const implementation = fs.readFileSync(path.join(__dirname, "core", "migration-backup.js"), "utf8");
386
+ const docs = fs.readFileSync(path.join(__dirname, "docs", "PROVIDER_MIGRATION.md"), "utf8");
387
+ const preflight = bot.indexOf("ensureMigrationSnapshot({");
388
+ const activation = bot.indexOf("completeProviderMigration({");
389
+ assert.ok(preflight >= 0);
390
+ assert.ok(activation > preflight, "provider-aware migration must follow the rollback snapshot");
391
+ for (const later of [
392
+ 'require("./core/vault-store")',
393
+ 'require("./core/scheduler")',
394
+ 'require("./core/router")',
395
+ "registry.bootstrap()",
396
+ ]) {
397
+ assert.ok(bot.indexOf(later) > activation, `provider migration must complete before ${later}`);
398
+ }
399
+ const jobsWrite = jobs.slice(jobs.indexOf("function saveDocument"), jobs.indexOf("function ensureMigrated"));
400
+ assert.ok(jobsWrite.indexOf('requireMigrationSnapshotForComponent("jobs"') >= 0);
401
+ assert.ok(jobsWrite.indexOf('requireMigrationSnapshotForComponent("jobs"') < jobsWrite.indexOf('markMigrationComponentPrepared'));
402
+ assert.ok(jobsWrite.indexOf('markMigrationComponentPrepared') < jobsWrite.indexOf('atomicWriteFileSync'));
403
+ assert.ok(jobsWrite.indexOf('atomicWriteFileSync') < jobsWrite.indexOf('markMigrationComponentActivated'));
404
+ const cronMigration = scheduler.slice(scheduler.indexOf("function archiveLegacyCrons"), scheduler.indexOf("function init"));
405
+ assert.match(cronMigration, /jobsStore\.archive\(/);
406
+ assert.ok(!/renameSync|unlinkSync|rmSync/.test(cronMigration), "legacy cron source remains untouched after archival");
407
+ assert.ok(!/sourceRecord\(["']\.env/.test(implementation));
408
+ assert.ok(!/sourceRecord\(["'](?:vault|auth|identities)/.test(implementation));
409
+ assert.match(docs, /Stop every Open Claudia bot and scheduler process/);
410
+ assert.match(docs, /validateMigrationSnapshot/);
411
+ assert.match(docs, /rollbackMigrationSnapshot/);
412
+ assert.match(docs, /do not share, publish, or commit/i);
413
+ assert.match(docs, /Removed-provider[\s\S]*disabled archives[\s\S]*never reassigned/i);
414
+ }
415
+
416
+ fixtureMatrix();
417
+ interruptionProbe();
418
+ corruptionAndLockProbe();
419
+ activationProbe();
420
+ rollbackProbe();
421
+ sourceChangeProbe();
422
+ rawByteProbe();
423
+ integrationAndDocsProbe();
424
+ console.log("provider migration backup OK");