From af0e60e9eeea8981719f8f2f589995a0ed17e27b Mon Sep 17 00:00:00 2001 From: Andrei Hasna Date: Sat, 1 Aug 2026 19:42:20 +0300 Subject: [PATCH 1/3] chore: begin drain OPE53-00023 Agent: Silvanus From 3045d7a2c344a63906d77247c073f3b4192e6c0e Mon Sep 17 00:00:00 2001 From: Andrei Hasna Date: Sat, 1 Aug 2026 19:42:20 +0300 Subject: [PATCH 2/3] test: cover store and conflict agent branches Agent: Silvanus --- CHANGELOG.md | 12 +++ tests/conflict-agent.test.ts | 191 ++++++++++++++++++++++++++++++++ tests/store.test.ts | 203 +++++++++++++++++++++++++++++++++++ 3 files changed, 406 insertions(+) create mode 100644 tests/conflict-agent.test.ts create mode 100644 tests/store.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 094f8d4..bb87bd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Unreleased — direct store and conflict-agent test coverage + +- Added focused tests for every runtime export in `src/store.ts`, including + legacy-store collision handling, malformed inputs, atomic persistence, + lock cleanup/reentrancy, and ID boundaries. +- Added direct tests for `src/conflict-agent.ts` covering complete and limited + evidence, durable fake-run telemetry, missing provider credentials, and an + unknown conflict. The source resolver was not duplicated here because its + successful resolution, revision/citation evidence, ACL denials, and raw-byte + boundary are already exercised directly in the existing database and + open-files fixture suites. + ## Unreleased — the backend is chosen explicitly, and test egress is refused **BREAKING for the fleet flip.** `HASNA_KNOWLEDGE_API_URL` + diff --git a/tests/conflict-agent.test.ts b/tests/conflict-agent.test.ts new file mode 100644 index 0000000..ad4d96b --- /dev/null +++ b/tests/conflict-agent.test.ts @@ -0,0 +1,191 @@ +import { describe, expect, test } from 'bun:test'; +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { proposeKnowledgeSyncConflictResolutionWithAi } from '../src/conflict-agent'; +import { migrateKnowledgeDb, openKnowledgeDb } from '../src/knowledge-db'; +import { getKnowledgeSyncConflict, recordKnowledgeSyncConflict } from '../src/sync'; + +function createWikiConflict(options: { localRow?: boolean; remoteRow?: boolean } = {}) { + const dir = mkdtempSync(join(tmpdir(), 'knowledge-conflict-agent-')); + const dbPath = join(dir, 'knowledge.db'); + const pageId = 'page-1'; + migrateKnowledgeDb(dbPath); + if (options.localRow) { + const db = openKnowledgeDb(dbPath); + try { + db.query(` + INSERT INTO wiki_pages (id, path, title, metadata_json, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + `).run( + pageId, + 'wiki/page-1.md', + 'Local page', + JSON.stringify({ source_ref: 'open-files://file/local-page' }), + '2026-07-29T00:00:00.000Z', + '2026-07-29T00:00:00.000Z', + ); + } finally { + db.close(); + } + } + const conflict = recordKnowledgeSyncConflict(dbPath, { + entityKind: 'wiki_pages', + entityId: `id=${JSON.stringify(pageId)}`, + localMachineId: 'local-machine', + remoteMachineId: 'remote-machine', + localHash: 'sha256:local', + remoteHash: 'sha256:remote', + baseHash: 'sha256:base', + metadata: options.remoteRow ? { + remote_row: { + id: pageId, + path: 'wiki/page-1.md', + title: 'Remote page', + source_ref: 'open-files://file/remote-page', + }, + } : {}, + }); + return { dbPath, conflict }; +} + +describe('conflict proposal agent', () => { + test('builds and records a fake approval-gated manual merge from local and remote evidence', async () => { + const { dbPath, conflict } = createWikiConflict({ localRow: true, remoteRow: true }); + + const proposal = await proposeKnowledgeSyncConflictResolutionWithAi({ + dbPath, + id: conflict.id, + modelRef: 'anthropic:claude-sonnet-4-6', + fake: true, + now: new Date('2026-07-29T12:00:00.000Z'), + }); + + expect(proposal).toMatchObject({ + mode: 'ai', + requires_approval: true, + proposed_strategy: 'manual-merge', + confidence: 0.5, + proposed_patch: { + kind: 'manual_merge', + target: `wiki_pages:id=${JSON.stringify('page-1')}`, + strategy: 'manual-merge', + metadata: { + fake: true, + local_hash: 'sha256:local', + remote_hash: 'sha256:remote', + }, + }, + agent: { + generated: true, + provider: 'anthropic', + model: 'claude-sonnet-4-6', + }, + }); + expect(proposal.proposed_patch?.diff).toContain('Local page'); + expect(proposal.proposed_patch?.diff).toContain('Remote page'); + expect(proposal.citations.map((citation) => citation.ref)).toContain('open-files://file/remote-page'); + expect(proposal.agent?.usage.input_tokens).toBeGreaterThan(0); + expect(proposal.agent?.usage.output_tokens).toBeGreaterThan(0); + expect(proposal.warnings).not.toContain('remote_row_snapshot_unavailable'); + expect(getKnowledgeSyncConflict(dbPath, conflict.id)?.status).toBe('open'); + + const db = openKnowledgeDb(dbPath); + try { + const run = db.query<{ + status: string; + provider: string; + model: string; + cost_tokens: number; + metadata_json: string; + }, []>('SELECT status, provider, model, cost_tokens, metadata_json FROM runs').get(); + expect(run).toMatchObject({ + status: 'dry_run', + provider: 'anthropic', + model: 'claude-sonnet-4-6', + }); + expect(run?.cost_tokens).toBeGreaterThan(0); + expect(JSON.parse(run?.metadata_json ?? '{}')).toMatchObject({ + conflict_id: conflict.id, + fake: true, + proposed_strategy: 'manual-merge', + }); + expect(db.query<{ event: string }, []>('SELECT event FROM run_events ORDER BY rowid').all()).toEqual([ + { event: 'conflict_evidence_retrieved' }, + { event: 'fake_conflict_proposal_generated' }, + ]); + expect(db.query<{ n: number }, []>('SELECT COUNT(*) AS n FROM provider_usage').get()?.n).toBe(0); + } finally { + db.close(); + } + }); + + test('uses the limited-evidence fake strategy and reports a missing remote snapshot', async () => { + const { dbPath, conflict } = createWikiConflict(); + + const proposal = await proposeKnowledgeSyncConflictResolutionWithAi({ + dbPath, + id: conflict.id, + fake: true, + }); + + expect(proposal.proposed_patch).toMatchObject({ + kind: 'custom', + strategy: 'review-and-select', + diff: null, + }); + expect(proposal.warnings).toContain('remote_row_snapshot_unavailable'); + }); + + test('records a failed run and rethrows when provider credentials are missing', async () => { + const { dbPath, conflict } = createWikiConflict({ remoteRow: true }); + + await expect(proposeKnowledgeSyncConflictResolutionWithAi({ + dbPath, + id: conflict.id, + modelRef: 'openai:gpt-5-mini', + env: {}, + now: new Date('2026-07-29T12:00:00.000Z'), + })).rejects.toThrow('Missing OPENAI_API_KEY for openai'); + + const db = openKnowledgeDb(dbPath); + try { + const run = db.query<{ status: string; cost_tokens: number; metadata_json: string }, []>( + 'SELECT status, cost_tokens, metadata_json FROM runs', + ).get(); + expect(run?.status).toBe('failed'); + expect(run?.cost_tokens).toBeGreaterThan(0); + expect(JSON.parse(run?.metadata_json ?? '{}')).toMatchObject({ + conflict_id: conflict.id, + mode: 'ai', + error: expect.stringContaining('Missing OPENAI_API_KEY'), + }); + expect(db.query<{ event: string; level: string }, []>( + 'SELECT event, level FROM run_events ORDER BY rowid', + ).all()).toEqual([ + { event: 'conflict_evidence_retrieved', level: 'info' }, + { event: 'conflict_proposal_generation_failed', level: 'error' }, + ]); + } finally { + db.close(); + } + }); + + test('rejects an unknown conflict without creating a run', async () => { + const dir = mkdtempSync(join(tmpdir(), 'knowledge-conflict-agent-missing-')); + const dbPath = join(dir, 'knowledge.db'); + + await expect(proposeKnowledgeSyncConflictResolutionWithAi({ + dbPath, + id: 'missing-conflict', + fake: true, + })).rejects.toThrow('Sync conflict not found: missing-conflict'); + + const db = openKnowledgeDb(dbPath); + try { + expect(db.query<{ n: number }, []>('SELECT COUNT(*) AS n FROM runs').get()?.n).toBe(0); + } finally { + db.close(); + } + }); +}); diff --git a/tests/store.test.ts b/tests/store.test.ts new file mode 100644 index 0000000..40d5fe9 --- /dev/null +++ b/tests/store.test.ts @@ -0,0 +1,203 @@ +import { describe, expect, test } from 'bun:test'; +import { spawnSync } from 'node:child_process'; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + statSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { + ensureStore, + loadStore, + loadStoreIfExists, + makeId, + makeShortId, + saveStore, + withLock, + type KnowledgeItem, +} from '../src/store'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(__dirname, '..'); +const storeModuleUrl = pathToFileURL(join(repoRoot, 'src', 'store.ts')).href; + +function item(id: string, shortId?: string): KnowledgeItem { + return { + id, + short_id: shortId, + title: id, + content: `Content for ${id}`, + url: null, + tags: [], + created_at: '2026-07-29T00:00:00.000Z', + updated_at: '2026-07-29T00:00:00.000Z', + }; +} + +describe('JSON store', () => { + test('ensureStore creates an owner-only empty store and preserves an existing file', () => { + const dir = mkdtempSync(join(tmpdir(), 'knowledge-store-ensure-')); + const path = join(dir, 'nested', 'db.json'); + + ensureStore(path); + + expect(JSON.parse(readFileSync(path, 'utf8'))).toEqual({ items: [] }); + if (process.platform !== 'win32') expect(statSync(path).mode & 0o777).toBe(0o600); + + const existing = `${JSON.stringify({ items: [item('k_existing')] }, null, 2)}\n`; + writeFileSync(path, existing); + ensureStore(path); + expect(readFileSync(path, 'utf8')).toBe(existing); + }); + + test('imports valid legacy items while preserving collisions, invalid rows, and the legacy source', () => { + const home = mkdtempSync(join(tmpdir(), 'knowledge-store-import-')); + const legacyPath = join(home, '.open-knowledge', 'db.json'); + const canonicalPath = join(home, '.hasna', 'knowledge', 'db.json'); + mkdirSync(dirname(legacyPath), { recursive: true }); + mkdirSync(dirname(canonicalPath), { recursive: true }); + const canonical = { items: [item('k_existing', 'existing')] }; + const legacy = { + items: [ + item('k_existing', 'different'), + item('k_short_collision', 'existing'), + item('k_imported', 'imported'), + null, + ], + }; + const legacyContents = `${JSON.stringify(legacy, null, 2)}\n`; + writeFileSync(canonicalPath, `${JSON.stringify(canonical, null, 2)}\n`); + writeFileSync(legacyPath, legacyContents); + + const script = ` + const store = await import(${JSON.stringify(storeModuleUrl)}); + const result = store.importLegacyGlobalStore({ now: new Date('2026-07-29T12:34:56.789Z') }); + console.log(JSON.stringify({ default_path: store.defaultStorePath(), result })); + `; + const child = spawnSync(process.execPath, ['--eval', script], { + cwd: repoRoot, + env: { ...process.env, HOME: home, USERPROFILE: home }, + encoding: 'utf8', + }); + + expect(child.status).toBe(0); + expect(child.stderr).toBe(''); + const output = JSON.parse(child.stdout.trim()) as { + default_path: string; + result: { + ok: boolean; + imported: number; + skipped_existing: number; + skipped_invalid: number; + canonical_created: boolean; + backup_path: string | null; + report_path: string | null; + }; + }; + expect(output.default_path).toBe(canonicalPath); + expect(output.result).toMatchObject({ + ok: true, + imported: 1, + skipped_existing: 2, + skipped_invalid: 1, + canonical_created: false, + }); + expect(JSON.parse(readFileSync(canonicalPath, 'utf8')).items.map((entry: KnowledgeItem) => entry.id)).toEqual([ + 'k_existing', + 'k_imported', + ]); + expect(readFileSync(legacyPath, 'utf8')).toBe(legacyContents); + expect(output.result.backup_path).not.toBeNull(); + expect(output.result.report_path).not.toBeNull(); + expect(JSON.parse(readFileSync(output.result.backup_path!, 'utf8'))).toEqual(canonical); + expect(JSON.parse(readFileSync(output.result.report_path!, 'utf8'))).toMatchObject({ + imported: 1, + skipped_existing: 2, + }); + }); + + test('loadStoreIfExists distinguishes a missing file, invalid store shape, and valid contents', () => { + const dir = mkdtempSync(join(tmpdir(), 'knowledge-store-optional-')); + const path = join(dir, 'db.json'); + + expect(loadStoreIfExists(path)).toEqual({ exists: false, items: [] }); + + writeFileSync(path, JSON.stringify({ items: null })); + expect(loadStoreIfExists(path)).toEqual({ exists: true, items: [] }); + + writeFileSync(path, JSON.stringify({ items: [item('k_loaded')] })); + expect(loadStoreIfExists(path)).toEqual({ exists: true, items: [item('k_loaded')] }); + + writeFileSync(path, '{broken'); + expect(() => loadStoreIfExists(path)).toThrow(); + }); + + test('loadStore initializes missing files, rejects malformed JSON, and normalizes invalid shape', () => { + const dir = mkdtempSync(join(tmpdir(), 'knowledge-store-load-')); + const path = join(dir, 'db.json'); + + expect(loadStore(path)).toEqual({ items: [] }); + expect(existsSync(path)).toBe(true); + + writeFileSync(path, JSON.stringify({ items: 'not-an-array' })); + expect(loadStore(path)).toEqual({ items: [] }); + + writeFileSync(path, '{broken'); + expect(() => loadStore(path)).toThrow(); + }); + + test('saveStore atomically writes nested stores with owner-only permissions', () => { + const dir = mkdtempSync(join(tmpdir(), 'knowledge-store-save-')); + const path = join(dir, 'nested', 'db.json'); + + saveStore(path, { items: [item('k_saved')] }); + + expect(JSON.parse(readFileSync(path, 'utf8'))).toEqual({ items: [item('k_saved')] }); + expect(readFileSync(path, 'utf8').endsWith('\n')).toBe(true); + if (process.platform !== 'win32') expect(statSync(path).mode & 0o777).toBe(0o600); + expect(readdirSync(dirname(path))).toEqual(['db.json']); + }); + + test('withLock supports same-process reentrancy and always removes its lock', () => { + const dir = mkdtempSync(join(tmpdir(), 'knowledge-store-lock-')); + const path = join(dir, 'nested', 'db.json'); + + const result = withLock(path, () => withLock(path, () => 'nested result'), { createParent: true }); + expect(result).toBe('nested result'); + expect(existsSync(`${path}.lock`)).toBe(false); + + expect(() => withLock(path, () => { + throw new Error('callback failed'); + })).toThrow('callback failed'); + expect(existsSync(`${path}.lock`)).toBe(false); + }); + + test('withLock does not invoke the callback when its parent is missing and creation was not requested', () => { + const dir = mkdtempSync(join(tmpdir(), 'knowledge-store-lock-parent-')); + const path = join(dir, 'missing', 'db.json'); + let called = false; + + expect(() => withLock(path, () => { + called = true; + })).toThrow(); + expect(called).toBe(false); + expect(existsSync(`${path}.lock`)).toBe(false); + }); + + test('generates full and short IDs at their documented boundaries', () => { + const first = makeId(); + const second = makeId(); + + expect(first).toMatch(/^k_[a-z0-9]+_[a-z0-9]{6}$/); + expect(second).not.toBe(first); + expect(makeShortId('k_1234567890abcdef')).toBe('1234567890ab'); + expect(makeShortId('already-short')).toBe('already-shor'); + expect(makeShortId('')).toBe(''); + }); +}); From 118e705b2c66b4da81159f1eb1ad4e385a5f8aa8 Mon Sep 17 00:00:00 2001 From: Andrei Hasna Date: Sat, 1 Aug 2026 20:05:41 +0300 Subject: [PATCH 3/3] test: relax package validation timeout Agent: unresolved-account005 --- tests/package-release.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/package-release.test.ts b/tests/package-release.test.ts index 377cf97..8ac49e9 100644 --- a/tests/package-release.test.ts +++ b/tests/package-release.test.ts @@ -304,5 +304,5 @@ describe('public package release safety', () => { expect(summary.docsFiles).not.toContain(path); expect(summary.scriptsFiles).not.toContain(path); } - }); + }, 15_000); });