Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 39 additions & 15 deletions oxlint.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
import { defineConfig } from 'oxlint';

// Applied in the base config AND re-declared in the *.test.* override below
// (oxlint replaces, rather than merges, a rule's options per override) so the
// require() ban can be added for test files without silently dropping these.
const restrictedSyntax = [
{
selector:
"CallExpression[callee.name='useEffect'] UnaryExpression[operator='typeof'] > Identifier[name='window']",
message:
"Do not use `typeof window !== 'undefined'` inside useEffect; useEffect already runs client-side.",
},
{
selector:
"CallExpression[callee.name='useLayoutEffect'] UnaryExpression[operator='typeof'] > Identifier[name='window']",
message:
"Do not use `typeof window !== 'undefined'` inside useLayoutEffect; useLayoutEffect already runs client-side.",
},
];

// Test files execute as ES modules, where a CommonJS `require()` is not
// defined. Calls like `require('./foo.ts')` or `require('node:fs')` break (or
// resolve inconsistently) at runtime. Use a static `import`, `await import()`,
// or `createRequire(import.meta.url)` for a native/CJS addon. The second
// selector also catches member calls like `require.resolve(...)`. Since both
// selectors key on the literal name `require`, binding a `createRequire` result
// to any other name (e.g. `require_`) keeps the escape hatch available without
// re-tripping either pattern.
const noRequireInTests = {
selector: "CallExpression[callee.name='require'], CallExpression[callee.object.name='require']",
message:
'require() is not available in an ESM test module. Use a static import or await import(); for a native/CJS addon use createRequire(import.meta.url) bound to a name other than "require" (e.g. require_).',
};

export default defineConfig({
// The .agents/skills, .codex/skills entries under
// public/open-knowledge/ are real directories of per-skill symlinks back
Expand Down Expand Up @@ -28,21 +60,7 @@ export default defineConfig({
enforceForIfStatements: true,
},
],
'eslint-js/no-restricted-syntax': [
'error',
{
selector:
"CallExpression[callee.name='useEffect'] UnaryExpression[operator='typeof'] > Identifier[name='window']",
message:
"Do not use `typeof window !== 'undefined'` inside useEffect; useEffect already runs client-side.",
},
{
selector:
"CallExpression[callee.name='useLayoutEffect'] UnaryExpression[operator='typeof'] > Identifier[name='window']",
message:
"Do not use `typeof window !== 'undefined'` inside useLayoutEffect; useLayoutEffect already runs client-side.",
},
],
'eslint-js/no-restricted-syntax': ['error', ...restrictedSyntax],
// TODO(oxlint): enable in priority order as each backlog is audited.
// Correctness rules catch async/control-flow bugs; graduate these first.
'typescript/no-floating-promises': 'off',
Expand Down Expand Up @@ -77,5 +95,11 @@ export default defineConfig({
'typescript/no-deprecated': 'error',
},
},
{
files: ['**/*.test.{ts,tsx,cts,mts,mjs}'],
rules: {
'eslint-js/no-restricted-syntax': ['error', ...restrictedSyntax, noRequireInTests],
},
},
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
* HTTP conflicts.json) and that the lifecycle transitions are
* observable end to end through the integration harness.
*/
import { describe, expect, test } from 'bun:test';

import { execFile } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { promisify } from 'node:util';
import {
Expand All @@ -36,6 +36,7 @@ import {
getLogger,
restoreLifecycleFromConflictsJson,
} from '@inkeep/open-knowledge-server';
import { describe, expect, test } from 'vitest';
import { createTestClient, createTestServer, pollUntil, type TestServer } from './test-harness';

const execFileAsync = promisify(execFile);
Expand Down Expand Up @@ -376,7 +377,6 @@ describe('FR12: /api/sync/conflicts + /api/sync/status count parity', () => {
const { tmpdir } = await import('node:os');
const tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'ok-fr12-')));
cleanups.push(() => {
const { rmSync } = require('node:fs') as typeof import('node:fs');
rmSync(tmpDir, { recursive: true, force: true });
});

Expand Down
14 changes: 6 additions & 8 deletions packages/cli/src/native/mcp-toml-edit-binding.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { describe, expect, test } from 'bun:test';
import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import * as nativeConfig from '@inkeep/open-knowledge-native-config';
import { describe, expect, test } from 'vitest';

// Exercise the insert-only upsert/remove + symlink-resolution across the real
// JS<->Rust boundary by loading the built `.node` directly. The Rust unit tests
// cover the engine's document + path semantics exhaustively; this guards the
// napi marshalling the Rust tests cannot reach — the `McpEditResult` /
// `SymlinkWritePaths` struct return shapes (including `Option<String>` ->
// `undefined`), string in/out, and the throw on malformed input. It
// hard-requires the addon (rather than skipping when absent) so a gate that
// failed to build the binding fails loudly instead of vacuously passing,
// matching `toml-config-engine.test.ts`.
// `undefined`), string in/out, and the throw on malformed input. It statically
// imports the addon (rather than skipping when absent) so a gate that failed to
// build the binding fails loudly instead of vacuously passing.

interface McpEditResult {
text: string;
Expand All @@ -31,8 +30,7 @@ interface NativeMcpEditBinding {
resolveSymlinkWritePath(path: string): SymlinkWritePaths;
}

const require = createRequire(import.meta.url);
const binding = require('@inkeep/open-knowledge-native-config') as NativeMcpEditBinding;
const binding: NativeMcpEditBinding = nativeConfig;

const SERVER = 'open-knowledge';
const ENTRY = JSON.stringify({ command: '/bin/sh', args: ['-l', '-c', 'run-ok'] });
Expand Down
11 changes: 5 additions & 6 deletions packages/cli/src/native/symlink-resolve.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import * as nativeConfig from '@inkeep/open-knowledge-native-config';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { resolveHarnessWritePaths, type SymlinkWritePaths } from './symlink-resolve.ts';

// One contract suite run against BOTH backends — the pure-JS mirror and the
// native (conformance-tested) resolver loaded from the built `.node` — so the
// two implementations cannot drift: any divergence in chain-following or
// cycle-breaking fails the same assertions for one backend but not the other.
// The native binding is hard-required (not skipped when absent) so a gate that
// failed to build the addon fails loudly, matching the sibling binding tests.
// The native binding is statically imported (not skipped when absent) so a gate
// that failed to build the addon fails loudly.

interface NativeSymlinkBinding {
resolveSymlinkWritePath(path: string): { readPath?: string | null; writePath: string };
}

const require = createRequire(import.meta.url);
const nativeBinding = require('@inkeep/open-knowledge-native-config') as NativeSymlinkBinding;
const nativeBinding: NativeSymlinkBinding = nativeConfig;

const unix = process.platform !== 'win32';

Expand Down
5 changes: 2 additions & 3 deletions packages/desktop/tests/unit/ok-wrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// shape + exit code the wrapper emits when the bundled CLI or
// Electron binary is missing (drag-to-Trash lifecycle).

import { describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import { accessSync, constants } from 'node:fs';
import { accessSync, constants, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, test } from 'vitest';

const WRAPPER = join(import.meta.dir, '..', '..', 'resources', 'cli', 'bin', 'ok.sh');

Expand Down Expand Up @@ -91,7 +91,6 @@ describe('ok.sh wrapper', () => {
// rescope verbatim. Without quoting, bash re-splits on whitespace
// and only `--require` is captured; everything after is evaluated
// as an extra command in the script's environment.
const { readFileSync } = require('node:fs') as typeof import('node:fs');
const script = readFileSync(WRAPPER, 'utf8');
expect(script).toContain('export OK_NODE_OPTIONS="$NODE_OPTIONS"');
expect(script).toContain('unset NODE_OPTIONS');
Expand Down
19 changes: 14 additions & 5 deletions packages/server/src/api-rename-crash-injection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,23 @@
* Production builds elide these branches (NODE_ENV !== 'test' AND
* OK_TEST_RENAME_FAULT unset).
*/
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';

import {
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from 'node:fs';
import type { IncomingMessage, ServerResponse } from 'node:http';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { Readable } from 'node:stream';
import simpleGit from 'simple-git';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { createApiExtension } from './api-extension.ts';
import { BacklinkIndex } from './backlink-index.ts';
import { swapContributors } from './contributor-tracker.ts';
Expand Down Expand Up @@ -62,17 +72,16 @@ function makeRes(): { res: ServerResponse; captured: CapturedResponse } {

function buildFileIndex(contentDir: string): ReadonlyMap<string, FileIndexEntry> {
const index = new Map<string, FileIndexEntry>();
const fs = require('node:fs') as typeof import('node:fs');
function walk(dir: string) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = resolve(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === '.ok' || entry.name === '.git') continue;
walk(fullPath);
continue;
}
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
const stat = fs.statSync(fullPath);
const stat = statSync(fullPath);
const docName = fullPath.slice(contentDir.length + 1).replace(/\.md$/, '');
index.set(docName, { size: stat.size, modified: stat.mtime.toISOString() });
}
Expand Down
19 changes: 14 additions & 5 deletions packages/server/src/api-rename-log-emission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@
* writer's L2 commit.
* - Folder rename of N docs produces N entries with shared `groupId`.
*/
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';

import {
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from 'node:fs';
import type { IncomingMessage, ServerResponse } from 'node:http';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { Readable } from 'node:stream';
import simpleGit from 'simple-git';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { createApiExtension } from './api-extension.ts';
import { BacklinkIndex } from './backlink-index.ts';
import { recordContributor, swapContributors } from './contributor-tracker.ts';
Expand Down Expand Up @@ -52,16 +62,15 @@ function makeRes(): { res: ServerResponse; captured: CapturedResponse } {
function buildFileIndex(contentDir: string): ReadonlyMap<string, FileIndexEntry> {
const index = new Map<string, FileIndexEntry>();
function walk(dir: string) {
const fs = require('node:fs') as typeof import('node:fs');
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = resolve(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === '.ok' || entry.name === '.git') continue;
walk(fullPath);
continue;
}
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
const stat = fs.statSync(fullPath);
const stat = statSync(fullPath);
const docName = fullPath.slice(contentDir.length + 1).replace(/\.md$/, '');
index.set(docName, { size: stat.size, modified: stat.mtime.toISOString() });
}
Expand Down
7 changes: 3 additions & 4 deletions packages/server/src/conflict-storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
* Unit tests for ConflictStore — CRUD and resolve strategies.
*/

import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { LOCAL_DIR } from '@inkeep/open-knowledge-core';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { type ConflictEntry, ConflictStore } from './conflict-storage.ts';

// ─── Test helpers ─────────────────────────────────────────────────────────────
Expand All @@ -16,8 +17,6 @@ let storePath = '';

beforeEach(() => {
// Create unique temp dirs per test
const { mkdtempSync } = require('node:fs');
const { tmpdir } = require('node:os');
tmpDir = mkdtempSync(join(tmpdir(), 'conflict-store-test-'));
projectDir = join(tmpDir, 'project');
storePath = join(projectDir, '.ok', LOCAL_DIR, 'conflicts.json');
Expand Down
8 changes: 2 additions & 6 deletions packages/server/src/file-logger.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { afterEach, describe, expect, test } from 'bun:test';
import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import pino from 'pino';
import { flushFileLogger, MAX_FILE_SIZE } from './file-logger.ts';
import { afterEach, describe, expect, test } from 'vitest';
import { createFileLogger, flushFileLogger, MAX_FILE_SIZE } from './file-logger.ts';

const TEST_DIR = join(tmpdir(), `ok-file-logger-test-${process.pid}`);

Expand Down Expand Up @@ -69,7 +69,6 @@ describe('file logger', () => {
expect(statSync(filePath).size).toBeGreaterThan(MAX_FILE_SIZE);

// Import and test rotateIfNeeded via createFileLogger side effect
const { createFileLogger } = require('./file-logger.ts');
createFileLogger({ name: 'big', filePath });

const files = readdirSync(TEST_DIR).filter((f: string) => f.startsWith('big.log'));
Expand All @@ -80,7 +79,6 @@ describe('file logger', () => {
test('opens the destination synchronously so same-tick process.exit cannot race the open', () => {
mkdirSync(TEST_DIR, { recursive: true });
const filePath = join(TEST_DIR, 'sync-open.log');
const { createFileLogger } = require('./file-logger.ts');
const logger = createFileLogger({ name: 'sync-open', filePath });
// pino registers an exit hook that calls flushSync() on the destination.
// With an async open (sync: false), the fs.open callback that assigns the
Expand All @@ -98,7 +96,6 @@ describe('file logger', () => {

test('createFileLogger unrefs the deferred prune timer so it never blocks process exit', () => {
mkdirSync(TEST_DIR, { recursive: true });
const { createFileLogger } = require('./file-logger.ts');
let unrefCalls = 0;
let scheduledMs: number | undefined;
// Fake scheduler: records the unref() call (the load-bearing contract) and
Expand Down Expand Up @@ -131,7 +128,6 @@ describe('file logger', () => {
const prev = process.env.OK_LOG_LEVEL;
process.env.OK_LOG_LEVEL = 'info';
try {
const { createFileLogger } = require('./file-logger.ts');
const logger = createFileLogger({ name: 'flush', filePath });
logger.warn({ outcome: 'absent', host: 'github.com' }, '[auth] git-credential get');
await flushFileLogger(logger);
Expand Down
Loading