openvibers/openvibe.community Commits
Importer: only rows between the OpenVibe launch and the id-space fix can hold a Network id (--ambiguous-from, default 2026-08-17T21:10:29Z); earlier rows were migrated from HoboStreamer with Live ids
Changes
@@@ -4,7 +4,7 @@/*** Import OpenVibe.Media's paste export into Community's database.*-- * node scripts/import-pastes.js <bundle.json> [--dry-run] [--id-fix-cutoff 2026-08-20T03:00:26Z]++ * node scripts/import-pastes.js <bundle.json> [--dry-run] [--id-fix-cutoff 2026-08-20T03:00:26Z] [--ambiguous-from 2026-08-17T21:10:29Z]** Reads the same environment as the server (.env / /etc/openvibe/community.env):* COMMUNITY_DB_PATH, OV_NETWORK_INTERNAL_URL, OV_OAUTH_CLIENT_ID, OV_OAUTH_CLIENT_SECRET (the@@@ -17,17 +17,18 @@ const path = require('path');function usage(msg) {if (msg) console.error(msg);-- console.error('usage: node scripts/import-pastes.js <bundle.json> [--dry-run] [--id-fix-cutoff <ISO time>]');++ console.error('usage: node scripts/import-pastes.js <bundle.json> [--dry-run] [--id-fix-cutoff <ISO time>] [--ambiguous-from <ISO time>|none]');process.exit(msg ? 1 : 0);}function parseArgs(argv) {-- const out = { file: null, dryRun: false, cutoff: null };++ const out = { file: null, dryRun: false, cutoff: null, ambiguousFrom: undefined };for (let i = 0; i < argv.length; i++) {const a = argv[i];if (a === '--dry-run') out.dryRun = true;else if (a === '--id-fix-cutoff') { out.cutoff = argv[++i]; if (!out.cutoff) usage('--id-fix-cutoff needs a value'); }else if (a.startsWith('--id-fix-cutoff=')) out.cutoff = a.slice('--id-fix-cutoff='.length);++ else if (a === '--ambiguous-from') { const v = argv[++i]; if (!v) usage('--ambiguous-from needs a value'); out.ambiguousFrom = v === 'none' ? null : v; }else if (a === '-h' || a === '--help') usage();else if (a.startsWith('-')) usage(`unknown option ${a}`);else if (!out.file) out.file = a;@@@ -55,6 +56,7 @@ async function main() {// A dry run writes nothing at all, not even the display cache.resolveLegacy: (system, list) => network.resolveLegacy(system, list, { cache: !args.dryRun }),cutoff: args.cutoff || DEFAULT_CUTOFF,++ ...(args.ambiguousFrom !== undefined ? { ambiguousFrom: args.ambiguousFrom } : {}),dryRun: args.dryRun,source: `${bundle.app || 'media'}:${path.basename(args.file)}`,});@@@ -30,6 +30,8 @@ const store = require('./store');const FORMAT = 'openvibe.media.pastes-export';const DEFAULT_CUTOFF = '2026-08-20T03:00:26Z';++// ac0d6a4 "Initial OpenVibe release" (2026-08-17 14:10:29 -07:00): earlier rows came from HoboStreamer's database.++const DEFAULT_AMBIGUOUS_FROM = '2026-08-17T21:10:29Z';// Media rebuilt screenshot metadata itself, so the ai_moment flag Live sent was dropped for image// pastes; the frame file names Live's AI jobs upload are the remaining marker.const AI_FRAME_NAME = /^(ai-moment-vod\d+-\d+|live-\d+-\d+)\.jpe?g$/i;@@@ -77,10 +79,15 @@ function verifyBundle(bundle) {* opts.source label for migration_runs* → report*/--async function importBundle(db, bundle, { resolveLegacy, cutoff = DEFAULT_CUTOFF, dryRun = false, source = 'media' } = {}) {++async function importBundle(db, bundle, { resolveLegacy, cutoff = DEFAULT_CUTOFF, ambiguousFrom = DEFAULT_AMBIGUOUS_FROM, dryRun = false, source = 'media' } = {}) {verifyBundle(bundle);const cutoffMs = toMs(cutoff);if (!Number.isFinite(cutoffMs)) throw new ImportError(`bad cutoff ${cutoff}`);++ const fromMs = ambiguousFrom ? toMs(ambiguousFrom) : -Infinity;++ if (!Number.isFinite(fromMs) && fromMs !== -Infinity) throw new ImportError(`bad ambiguous-from ${ambiguousFrom}`);++ // Only rows written between the OpenVibe launch and the id-space fix can hold a Network id: rows from++ // before the launch were migrated from HoboStreamer's own database, whose user ids Live kept.++ const inWindow = (createdAt) => { const t = toMs(createdAt); return t >= fromMs && !(t >= cutoffMs); };const startedAt = new Date().toISOString();// ── 1. Resolve every legacy user id before the transaction (network calls) ──@@@ -89,7 +96,7 @@ async function importBundle(db, bundle, { resolveLegacy, cutoff = DEFAULT_CUTOFFconst note = (userId, createdAt) => {if (userId == null) return;liveIds.add(String(userId));-- if (!(toMs(createdAt) >= cutoffMs)) preCutoffIds.add(String(userId));++ if (inWindow(createdAt)) preCutoffIds.add(String(userId));};for (const p of bundle.pastes) if (!isAiPaste(p)) note(p.user_id, p.created_at);for (const c of bundle.comments) note(c.user_id, c.created_at);@@@ -102,7 +109,7 @@ async function importBundle(db, bundle, { resolveLegacy, cutoff = DEFAULT_CUTOFFif (userId == null) return { subject: null, hold: null };const key = String(userId);const live = subjectOf(liveMap.get(key));-- const pre = !(toMs(createdAt) >= cutoffMs);++ const pre = inWindow(createdAt);if (!pre) return live ? { subject: live, hold: null } : { subject: null, hold: 'owner_unmapped', detail: { user_id: userId, created_at: createdAt, era: 'post-cutoff' } };const net = subjectOf(netMap.get(key));if (live && net && live !== net) return { subject: null, hold: 'ambiguous_owner', detail: { user_id: userId, created_at: createdAt, live_subject: live, network_subject: net } };@@@ -113,7 +120,7 @@ async function importBundle(db, bundle, { resolveLegacy, cutoff = DEFAULT_CUTOFFconst blank = () => ({ bundle: 0, inserted: 0, updated: 0, unchanged: 0, held: 0, skipped: 0 });const report = {source, app: bundle.app || null, generated_at: bundle.generated_at || null, since_id: bundle.since_id ?? null, max_id: bundle.max_id ?? null,-- cutoff, dry_run: !!dryRun,++ cutoff, ambiguous_from: ambiguousFrom || null, dry_run: !!dryRun,pastes: { ...blank(), bundle: bundle.pastes.length, ai: 0, forks_remapped: 0, forks_unresolved: 0 },comments: { ...blank(), bundle: bundle.comments.length },likes: { ...blank(), bundle: bundle.likes.length },@@@ -304,16 +311,16 @@ function formatReport(r) {const line = (name, t) => ` ${name.padEnd(9)} ${String(t.bundle).padStart(7)} ${String(t.inserted).padStart(9)} ${String(t.updated).padStart(8)} ${String(t.unchanged).padStart(10)} ${String(t.held).padStart(6)} ${String(t.skipped).padStart(8)}`;const out = [`Paste import from ${r.source}${r.dry_run ? ' (DRY RUN — rolled back)' : ''}`,-- ` bundle app=${r.app} ids ${r.since_id ?? '?'}..${r.max_id ?? '?'} generated ${r.generated_at || '?'}; id-space cutoff ${r.cutoff}`,++ ` bundle app=${r.app} ids ${r.since_id ?? '?'}..${r.max_id ?? '?'} generated ${r.generated_at || '?'}; ambiguous id window ${r.ambiguous_from || 'start'} .. ${r.cutoff}`,'',` ${'table'.padEnd(9)} ${'bundle'.padStart(7)} ${'inserted'.padStart(9)} ${'updated'.padStart(8)} ${'unchanged'.padStart(10)} ${'held'.padStart(6)} ${'skipped'.padStart(8)}`,line('pastes', r.pastes), line('comments', r.comments), line('likes', r.likes),'',` AI pastes (ownerless): ${r.pastes.ai}; forks remapped: ${r.pastes.forks_remapped}, unresolved: ${r.pastes.forks_unresolved}`,-- ` users: ${r.users.live_ids} live ids (${r.users.live_mapped} mapped), ${r.users.network_checked} pre-cutoff ids checked as network ids (${r.users.network_mapped} mapped)`,++ ` users: ${r.users.live_ids} live ids (${r.users.live_mapped} mapped), ${r.users.network_checked} in-window ids checked as network ids (${r.users.network_mapped} mapped)`,` holds: ${Object.keys(r.holds).length ? Object.entries(r.holds).map(([k, v]) => `${k}=${v}`).join(', ') : 'none'}`,];return out.join('\n');}--module.exports = { importBundle, verifyBundle, formatReport, isAiPaste, ImportError, FORMAT, DEFAULT_CUTOFF };++module.exports = { DEFAULT_AMBIGUOUS_FROM, importBundle, verifyBundle, formatReport, isAiPaste, ImportError, FORMAT, DEFAULT_CUTOFF };@@@ -16,7 +16,8 @@ const { createNetworkIdentity } = require('../server/identity/network');const mockNetwork = require('./helpers/mock-network');const { check, done } = require('./helpers/app');--const PRE = '2026-08-01 10:00:00'; // before the id-space fix (2026-08-20T03:00:26Z)++const PRE = '2026-08-18 10:00:00'; // between the OpenVibe launch (2026-08-17T21:10:29Z) and the id-space fix (2026-08-20T03:00:26Z)++const MIGRATED = '2026-08-01 10:00:00'; // before the launch: rows migrated from HoboStreamer, Live idsconst POST = '2026-09-01 10:00:00';function seal(bundle) {@@@ -225,6 +226,17 @@ const paste = (o) => ({assert.strictEqual(r.pastes.updated, 2, 'the renamed paste and the newly owned one');});++ await check('rows from before the OpenVibe launch map by Live id even when the number is also a Network id', async () => {++ const mdb = openDb(':memory:');++ const r = await importBundle(mdb, seal({ pastes: [paste({ id: 70, slug: 'old-hobo-70', user_id: 9, created_at: MIGRATED, updated_at: MIGRATED })], comments: [], likes: [] }), { resolveLegacy: identity.resolveLegacy, source: 'test' });++ const row = mdb.prepare('SELECT owner_subject FROM pastes WHERE slug = ?').get('old-hobo-70');++ assert.strictEqual(row.owner_subject, C.subject_id, 'live 9 = cleo, not network 9 = sam');++ assert.strictEqual(r.pastes.held, 0);++ const r2 = await importBundle(openDb(':memory:'), seal({ pastes: [paste({ id: 70, slug: 'old-hobo-70', user_id: 9, created_at: MIGRATED, updated_at: MIGRATED })], comments: [], likes: [] }), { resolveLegacy: identity.resolveLegacy, source: 'test', ambiguousFrom: null });++ assert.strictEqual(r2.pastes.held, 1, 'ambiguousFrom: null restores the whole-history window');++ mdb.close();++ });++await check('CLI: exit 2 on a refused bundle, dry run prints the report and writes nothing', async () => {const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'community-import-'));const dbPath = path.join(dir, 'c.db');
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
/**
/**
* Import OpenVibe.Media's paste export into Community's database.
* Import OpenVibe.Media's paste export into Community's database.
*
*
- * node scripts/import-pastes.js <bundle.json> [--dry-run] [--id-fix-cutoff 2026-08-20T03:00:26Z]
+ * node scripts/import-pastes.js <bundle.json> [--dry-run] [--id-fix-cutoff 2026-08-20T03:00:26Z] [--ambiguous-from 2026-08-17T21:10:29Z]
*
*
* Reads the same environment as the server (.env / /etc/openvibe/community.env):
* Reads the same environment as the server (.env / /etc/openvibe/community.env):
* COMMUNITY_DB_PATH, OV_NETWORK_INTERNAL_URL, OV_OAUTH_CLIENT_ID, OV_OAUTH_CLIENT_SECRET (the
* COMMUNITY_DB_PATH, OV_NETWORK_INTERNAL_URL, OV_OAUTH_CLIENT_ID, OV_OAUTH_CLIENT_SECRET (the
@@ -17,17 +17,18 @@ const path = require('path');
@@ -17,17 +17,18 @@ const path = require('path');
function usage(msg) {
function usage(msg) {
if (msg) console.error(msg);
if (msg) console.error(msg);
- console.error('usage: node scripts/import-pastes.js <bundle.json> [--dry-run] [--id-fix-cutoff <ISO time>]');
+ console.error('usage: node scripts/import-pastes.js <bundle.json> [--dry-run] [--id-fix-cutoff <ISO time>] [--ambiguous-from <ISO time>|none]');
process.exit(msg ? 1 : 0);
process.exit(msg ? 1 : 0);
}
}
function parseArgs(argv) {
function parseArgs(argv) {
- const out = { file: null, dryRun: false, cutoff: null };
+ const out = { file: null, dryRun: false, cutoff: null, ambiguousFrom: undefined };
for (let i = 0; i < argv.length; i++) {
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
const a = argv[i];
if (a === '--dry-run') out.dryRun = true;
if (a === '--dry-run') out.dryRun = true;
else if (a === '--id-fix-cutoff') { out.cutoff = argv[++i]; if (!out.cutoff) usage('--id-fix-cutoff needs a value'); }
else if (a === '--id-fix-cutoff') { out.cutoff = argv[++i]; if (!out.cutoff) usage('--id-fix-cutoff needs a value'); }
else if (a.startsWith('--id-fix-cutoff=')) out.cutoff = a.slice('--id-fix-cutoff='.length);
else if (a.startsWith('--id-fix-cutoff=')) out.cutoff = a.slice('--id-fix-cutoff='.length);
+ else if (a === '--ambiguous-from') { const v = argv[++i]; if (!v) usage('--ambiguous-from needs a value'); out.ambiguousFrom = v === 'none' ? null : v; }
else if (a === '-h' || a === '--help') usage();
else if (a === '-h' || a === '--help') usage();
else if (a.startsWith('-')) usage(`unknown option ${a}`);
else if (a.startsWith('-')) usage(`unknown option ${a}`);
else if (!out.file) out.file = a;
else if (!out.file) out.file = a;
@@ -55,6 +56,7 @@ async function main() {
@@ -55,6 +56,7 @@ async function main() {
// A dry run writes nothing at all, not even the display cache.
// A dry run writes nothing at all, not even the display cache.
resolveLegacy: (system, list) => network.resolveLegacy(system, list, { cache: !args.dryRun }),
resolveLegacy: (system, list) => network.resolveLegacy(system, list, { cache: !args.dryRun }),
cutoff: args.cutoff || DEFAULT_CUTOFF,
cutoff: args.cutoff || DEFAULT_CUTOFF,
+ ...(args.ambiguousFrom !== undefined ? { ambiguousFrom: args.ambiguousFrom } : {}),
dryRun: args.dryRun,
dryRun: args.dryRun,
source: `${bundle.app || 'media'}:${path.basename(args.file)}`,
source: `${bundle.app || 'media'}:${path.basename(args.file)}`,
});
});
@@ -30,6 +30,8 @@ const store = require('./store');
@@ -30,6 +30,8 @@ const store = require('./store');
const FORMAT = 'openvibe.media.pastes-export';
const FORMAT = 'openvibe.media.pastes-export';
const DEFAULT_CUTOFF = '2026-08-20T03:00:26Z';
const DEFAULT_CUTOFF = '2026-08-20T03:00:26Z';
+// ac0d6a4 "Initial OpenVibe release" (2026-08-17 14:10:29 -07:00): earlier rows came from HoboStreamer's database.
+const DEFAULT_AMBIGUOUS_FROM = '2026-08-17T21:10:29Z';
// Media rebuilt screenshot metadata itself, so the ai_moment flag Live sent was dropped for image
// Media rebuilt screenshot metadata itself, so the ai_moment flag Live sent was dropped for image
// pastes; the frame file names Live's AI jobs upload are the remaining marker.
// pastes; the frame file names Live's AI jobs upload are the remaining marker.
const AI_FRAME_NAME = /^(ai-moment-vod\d+-\d+|live-\d+-\d+)\.jpe?g$/i;
const AI_FRAME_NAME = /^(ai-moment-vod\d+-\d+|live-\d+-\d+)\.jpe?g$/i;
@@ -77,10 +79,15 @@ function verifyBundle(bundle) {
@@ -77,10 +79,15 @@ function verifyBundle(bundle) {
* opts.source label for migration_runs
* opts.source label for migration_runs
* → report
* → report
*/
*/
-async function importBundle(db, bundle, { resolveLegacy, cutoff = DEFAULT_CUTOFF, dryRun = false, source = 'media' } = {}) {
+async function importBundle(db, bundle, { resolveLegacy, cutoff = DEFAULT_CUTOFF, ambiguousFrom = DEFAULT_AMBIGUOUS_FROM, dryRun = false, source = 'media' } = {}) {
verifyBundle(bundle);
verifyBundle(bundle);
const cutoffMs = toMs(cutoff);
const cutoffMs = toMs(cutoff);
if (!Number.isFinite(cutoffMs)) throw new ImportError(`bad cutoff ${cutoff}`);
if (!Number.isFinite(cutoffMs)) throw new ImportError(`bad cutoff ${cutoff}`);
+ const fromMs = ambiguousFrom ? toMs(ambiguousFrom) : -Infinity;
+ if (!Number.isFinite(fromMs) && fromMs !== -Infinity) throw new ImportError(`bad ambiguous-from ${ambiguousFrom}`);
+ // Only rows written between the OpenVibe launch and the id-space fix can hold a Network id: rows from
+ // before the launch were migrated from HoboStreamer's own database, whose user ids Live kept.
+ const inWindow = (createdAt) => { const t = toMs(createdAt); return t >= fromMs && !(t >= cutoffMs); };
const startedAt = new Date().toISOString();
const startedAt = new Date().toISOString();
// ── 1. Resolve every legacy user id before the transaction (network calls) ──
// ── 1. Resolve every legacy user id before the transaction (network calls) ──
@@ -89,7 +96,7 @@ async function importBundle(db, bundle, { resolveLegacy, cutoff = DEFAULT_CUTOFF
@@ -89,7 +96,7 @@ async function importBundle(db, bundle, { resolveLegacy, cutoff = DEFAULT_CUTOFF
const note = (userId, createdAt) => {
const note = (userId, createdAt) => {
if (userId == null) return;
if (userId == null) return;
liveIds.add(String(userId));
liveIds.add(String(userId));
- if (!(toMs(createdAt) >= cutoffMs)) preCutoffIds.add(String(userId));
+ if (inWindow(createdAt)) preCutoffIds.add(String(userId));
};
};
for (const p of bundle.pastes) if (!isAiPaste(p)) note(p.user_id, p.created_at);
for (const p of bundle.pastes) if (!isAiPaste(p)) note(p.user_id, p.created_at);
for (const c of bundle.comments) note(c.user_id, c.created_at);
for (const c of bundle.comments) note(c.user_id, c.created_at);
@@ -102,7 +109,7 @@ async function importBundle(db, bundle, { resolveLegacy, cutoff = DEFAULT_CUTOFF
@@ -102,7 +109,7 @@ async function importBundle(db, bundle, { resolveLegacy, cutoff = DEFAULT_CUTOFF
if (userId == null) return { subject: null, hold: null };
if (userId == null) return { subject: null, hold: null };
const key = String(userId);
const key = String(userId);
const live = subjectOf(liveMap.get(key));
const live = subjectOf(liveMap.get(key));
- const pre = !(toMs(createdAt) >= cutoffMs);
+ const pre = inWindow(createdAt);
if (!pre) return live ? { subject: live, hold: null } : { subject: null, hold: 'owner_unmapped', detail: { user_id: userId, created_at: createdAt, era: 'post-cutoff' } };
if (!pre) return live ? { subject: live, hold: null } : { subject: null, hold: 'owner_unmapped', detail: { user_id: userId, created_at: createdAt, era: 'post-cutoff' } };
const net = subjectOf(netMap.get(key));
const net = subjectOf(netMap.get(key));
if (live && net && live !== net) return { subject: null, hold: 'ambiguous_owner', detail: { user_id: userId, created_at: createdAt, live_subject: live, network_subject: net } };
if (live && net && live !== net) return { subject: null, hold: 'ambiguous_owner', detail: { user_id: userId, created_at: createdAt, live_subject: live, network_subject: net } };
@@ -113,7 +120,7 @@ async function importBundle(db, bundle, { resolveLegacy, cutoff = DEFAULT_CUTOFF
@@ -113,7 +120,7 @@ async function importBundle(db, bundle, { resolveLegacy, cutoff = DEFAULT_CUTOFF
const blank = () => ({ bundle: 0, inserted: 0, updated: 0, unchanged: 0, held: 0, skipped: 0 });
const blank = () => ({ bundle: 0, inserted: 0, updated: 0, unchanged: 0, held: 0, skipped: 0 });
const report = {
const report = {
source, app: bundle.app || null, generated_at: bundle.generated_at || null, since_id: bundle.since_id ?? null, max_id: bundle.max_id ?? null,
source, app: bundle.app || null, generated_at: bundle.generated_at || null, since_id: bundle.since_id ?? null, max_id: bundle.max_id ?? null,
- cutoff, dry_run: !!dryRun,
+ cutoff, ambiguous_from: ambiguousFrom || null, dry_run: !!dryRun,
pastes: { ...blank(), bundle: bundle.pastes.length, ai: 0, forks_remapped: 0, forks_unresolved: 0 },
pastes: { ...blank(), bundle: bundle.pastes.length, ai: 0, forks_remapped: 0, forks_unresolved: 0 },
comments: { ...blank(), bundle: bundle.comments.length },
comments: { ...blank(), bundle: bundle.comments.length },
likes: { ...blank(), bundle: bundle.likes.length },
likes: { ...blank(), bundle: bundle.likes.length },
@@ -304,16 +311,16 @@ function formatReport(r) {
@@ -304,16 +311,16 @@ function formatReport(r) {
const line = (name, t) => ` ${name.padEnd(9)} ${String(t.bundle).padStart(7)} ${String(t.inserted).padStart(9)} ${String(t.updated).padStart(8)} ${String(t.unchanged).padStart(10)} ${String(t.held).padStart(6)} ${String(t.skipped).padStart(8)}`;
const line = (name, t) => ` ${name.padEnd(9)} ${String(t.bundle).padStart(7)} ${String(t.inserted).padStart(9)} ${String(t.updated).padStart(8)} ${String(t.unchanged).padStart(10)} ${String(t.held).padStart(6)} ${String(t.skipped).padStart(8)}`;
const out = [
const out = [
`Paste import from ${r.source}${r.dry_run ? ' (DRY RUN — rolled back)' : ''}`,
`Paste import from ${r.source}${r.dry_run ? ' (DRY RUN — rolled back)' : ''}`,
- ` bundle app=${r.app} ids ${r.since_id ?? '?'}..${r.max_id ?? '?'} generated ${r.generated_at || '?'}; id-space cutoff ${r.cutoff}`,
+ ` bundle app=${r.app} ids ${r.since_id ?? '?'}..${r.max_id ?? '?'} generated ${r.generated_at || '?'}; ambiguous id window ${r.ambiguous_from || 'start'} .. ${r.cutoff}`,
'',
'',
` ${'table'.padEnd(9)} ${'bundle'.padStart(7)} ${'inserted'.padStart(9)} ${'updated'.padStart(8)} ${'unchanged'.padStart(10)} ${'held'.padStart(6)} ${'skipped'.padStart(8)}`,
` ${'table'.padEnd(9)} ${'bundle'.padStart(7)} ${'inserted'.padStart(9)} ${'updated'.padStart(8)} ${'unchanged'.padStart(10)} ${'held'.padStart(6)} ${'skipped'.padStart(8)}`,
line('pastes', r.pastes), line('comments', r.comments), line('likes', r.likes),
line('pastes', r.pastes), line('comments', r.comments), line('likes', r.likes),
'',
'',
` AI pastes (ownerless): ${r.pastes.ai}; forks remapped: ${r.pastes.forks_remapped}, unresolved: ${r.pastes.forks_unresolved}`,
` AI pastes (ownerless): ${r.pastes.ai}; forks remapped: ${r.pastes.forks_remapped}, unresolved: ${r.pastes.forks_unresolved}`,
- ` users: ${r.users.live_ids} live ids (${r.users.live_mapped} mapped), ${r.users.network_checked} pre-cutoff ids checked as network ids (${r.users.network_mapped} mapped)`,
+ ` users: ${r.users.live_ids} live ids (${r.users.live_mapped} mapped), ${r.users.network_checked} in-window ids checked as network ids (${r.users.network_mapped} mapped)`,
` holds: ${Object.keys(r.holds).length ? Object.entries(r.holds).map(([k, v]) => `${k}=${v}`).join(', ') : 'none'}`,
` holds: ${Object.keys(r.holds).length ? Object.entries(r.holds).map(([k, v]) => `${k}=${v}`).join(', ') : 'none'}`,
];
];
return out.join('\n');
return out.join('\n');
}
}
-module.exports = { importBundle, verifyBundle, formatReport, isAiPaste, ImportError, FORMAT, DEFAULT_CUTOFF };
+module.exports = { DEFAULT_AMBIGUOUS_FROM, importBundle, verifyBundle, formatReport, isAiPaste, ImportError, FORMAT, DEFAULT_CUTOFF };
@@ -16,7 +16,8 @@ const { createNetworkIdentity } = require('../server/identity/network');
@@ -16,7 +16,8 @@ const { createNetworkIdentity } = require('../server/identity/network');
const mockNetwork = require('./helpers/mock-network');
const mockNetwork = require('./helpers/mock-network');
const { check, done } = require('./helpers/app');
const { check, done } = require('./helpers/app');
-const PRE = '2026-08-01 10:00:00'; // before the id-space fix (2026-08-20T03:00:26Z)
+const PRE = '2026-08-18 10:00:00'; // between the OpenVibe launch (2026-08-17T21:10:29Z) and the id-space fix (2026-08-20T03:00:26Z)
+const MIGRATED = '2026-08-01 10:00:00'; // before the launch: rows migrated from HoboStreamer, Live ids
const POST = '2026-09-01 10:00:00';
const POST = '2026-09-01 10:00:00';
function seal(bundle) {
function seal(bundle) {
@@ -225,6 +226,17 @@ const paste = (o) => ({
@@ -225,6 +226,17 @@ const paste = (o) => ({
assert.strictEqual(r.pastes.updated, 2, 'the renamed paste and the newly owned one');
assert.strictEqual(r.pastes.updated, 2, 'the renamed paste and the newly owned one');
});
});
+ await check('rows from before the OpenVibe launch map by Live id even when the number is also a Network id', async () => {
+ const mdb = openDb(':memory:');
+ const r = await importBundle(mdb, seal({ pastes: [paste({ id: 70, slug: 'old-hobo-70', user_id: 9, created_at: MIGRATED, updated_at: MIGRATED })], comments: [], likes: [] }), { resolveLegacy: identity.resolveLegacy, source: 'test' });
+ const row = mdb.prepare('SELECT owner_subject FROM pastes WHERE slug = ?').get('old-hobo-70');
+ assert.strictEqual(row.owner_subject, C.subject_id, 'live 9 = cleo, not network 9 = sam');
+ assert.strictEqual(r.pastes.held, 0);
+ const r2 = await importBundle(openDb(':memory:'), seal({ pastes: [paste({ id: 70, slug: 'old-hobo-70', user_id: 9, created_at: MIGRATED, updated_at: MIGRATED })], comments: [], likes: [] }), { resolveLegacy: identity.resolveLegacy, source: 'test', ambiguousFrom: null });
+ assert.strictEqual(r2.pastes.held, 1, 'ambiguousFrom: null restores the whole-history window');
+ mdb.close();
+ });
+
await check('CLI: exit 2 on a refused bundle, dry run prints the report and writes nothing', async () => {
await check('CLI: exit 2 on a refused bundle, dry run prints the report and writes nothing', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'community-import-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'community-import-'));
const dbPath = path.join(dir, 'c.db');
const dbPath = path.join(dir, 'c.db');