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
141 changes: 102 additions & 39 deletions oxlint.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,41 @@
import nkzw from '@nkzw/oxlint-config';
import { defineConfig } from 'oxlint';

// Product source trees, where unit tests get scratch from the package tmp-dir helpers.
// scripts/, test/, and package test harnesses manage TMPDIR for child processes on purpose,
// so keeping the roots explicit holds them out of scope that a bare `**` would sweep in.
const PRODUCT_TEST_ROOTS = ['src', 'packages/*/src'];
const PRODUCT_TEST_SHAPES = [
'**/*.test.ts',
'**/*.fixtures.ts',
'**/__tests__/**/*.ts',
'**/test-utils/**/*.ts',
];
const PRODUCT_TEST_FILES = PRODUCT_TEST_ROOTS.flatMap((root) =>
PRODUCT_TEST_SHAPES.map((shape) => `${root}/${shape}`),
);

// The last matching override replaces `no-restricted-imports` options instead of merging them,
// so each override restates every ban that applies to the files it matches.
const CHILD_PROCESS_PATH = {
message:
'Use process helpers from @agent-device/host-kit/command instead of importing node:child_process directly.',
name: 'node:child_process',
};
const PROVIDER_IMPORT_PATTERN = {
group: ['@agent-device/provider-*'],
message:
'Command implementations must ask src/cli/connection/provider-policy.ts for provider capabilities.',
};
const NODE_OS_PATH = {
message:
'Create test scratch with mkdtempForTest()/mkdtempForTestSync() from the package tmp-dir helper instead of reading node:os.',
name: 'node:os',
};

const CHILD_PROCESS_BAN_ROOTS = ['src', 'packages/host-kit/src'];
const PROVIDER_BAN_ROOTS = ['src/commands', 'src/cli/commands'];

export default defineConfig({
env: {
builtin: true,
Expand Down Expand Up @@ -55,57 +90,23 @@ export default defineConfig({
},
},
{
files: ['src/**/*.ts', 'packages/host-kit/src/**/*.ts'],
files: CHILD_PROCESS_BAN_ROOTS.map((root) => `${root}/**/*.ts`),
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'node:child_process',
message:
'Use process helpers from @agent-device/host-kit/command instead of importing node:child_process directly.',
},
],
},
],
'no-restricted-imports': ['error', { paths: [CHILD_PROCESS_PATH] }],
},
},
{
files: ['src/commands/**/*.ts', 'src/cli/commands/**/*.ts'],
files: PROVIDER_BAN_ROOTS.map((root) => `${root}/**/*.ts`),
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'node:child_process',
message:
'Use process helpers from @agent-device/host-kit/command instead of importing node:child_process directly.',
},
],
patterns: [
{
group: ['@agent-device/provider-*'],
message:
'Command implementations must ask src/cli/connection/provider-policy.ts for provider capabilities.',
},
],
paths: [CHILD_PROCESS_PATH],
patterns: [PROVIDER_IMPORT_PATTERN],
},
],
},
},
{
files: [
'packages/host-kit/src/internal/exec.ts',
'packages/host-kit/src/**/*.test.ts',
'src/**/*.test.ts',
'src/**/__tests__/**/*.ts',
],
rules: {
'no-restricted-imports': ['error', { paths: [] }],
},
},
{
files: ['examples/test-app/src/**/*.tsx'],
rules: {
Expand Down Expand Up @@ -146,5 +147,67 @@ export default defineConfig({
output: 'writable',
},
},
{
// Product tests get scratch from the package tmp-dir helpers, which honor the run's
// redirected TMPDIR; reading node:os reuses a fixed path across the suite. A file with a
// justified read (mocking production, a real socket path, or the TMPDIR mechanism itself)
// oxlint-disables its one import line with a reason instead of widening this list.
files: PRODUCT_TEST_FILES,
rules: {
'no-restricted-imports': ['error', { paths: [NODE_OS_PATH] }],
},
},
{
files: CHILD_PROCESS_BAN_ROOTS.flatMap((root) =>
PRODUCT_TEST_SHAPES.map((shape) => `${root}/${shape}`),
),
rules: {
'no-restricted-imports': ['error', { paths: [CHILD_PROCESS_PATH, NODE_OS_PATH] }],
},
},
{
files: PROVIDER_BAN_ROOTS.flatMap((root) =>
PRODUCT_TEST_SHAPES.map((shape) => `${root}/${shape}`),
),
rules: {
'no-restricted-imports': [
'error',
{
paths: [CHILD_PROCESS_PATH, NODE_OS_PATH],
patterns: [PROVIDER_IMPORT_PATTERN],
},
],
},
},
{
// Tests may import node:child_process and provider packages directly.
files: ['packages/host-kit/src/**/*.test.ts', 'src/**/*.test.ts', 'src/**/__tests__/**/*.ts'],
rules: {
'no-restricted-imports': ['error', { paths: [NODE_OS_PATH] }],
},
},
{
// The tmp-dir helper modules are the sanctioned os.tmpdir() readers.
files: ['**/tmp-dir.ts', '**/tmp-dir.fixtures.ts'],
rules: {
'no-restricted-imports': ['error', { paths: [] }],
},
},
{
files: CHILD_PROCESS_BAN_ROOTS.flatMap((root) => [
`${root}/**/tmp-dir.ts`,
`${root}/**/tmp-dir.fixtures.ts`,
]),
rules: {
'no-restricted-imports': ['error', { paths: [CHILD_PROCESS_PATH] }],
},
},
{
// The host-kit process helpers are the sanctioned node:child_process importer.
files: ['packages/host-kit/src/internal/exec.ts'],
rules: {
'no-restricted-imports': ['error', { paths: [] }],
},
},
],
});
5 changes: 3 additions & 2 deletions packages/capture-kit/src/audio-probe-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import { promises as fs } from 'node:fs';
import os from 'node:os';

import path from 'node:path';
import { test } from 'vitest';
import type {
Expand All @@ -19,6 +19,7 @@ import {
createDurableResourceEnvelope,
encodeDurableDescriptor,
} from './durable-resource-envelope.ts';
import { mkdtempForTest } from './tmp-dir.fixtures.ts';

const device: DeviceInfo = {
id: 'macos-host',
Expand Down Expand Up @@ -56,7 +57,7 @@ function envelopeWith(body: object) {
}

async function withStatusDir<T>(run: (dir: string) => Promise<T>): Promise<T> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'audio-probe-kit-'));
const dir = await mkdtempForTest('audio-probe-kit-');
try {
return await run(dir);
} finally {
Expand Down
1 change: 1 addition & 0 deletions packages/host-kit/src/code-signature-cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
// oxlint-disable-next-line no-restricted-imports -- vi.spyOn(os,'tmpdir') aims production's cache home
import os from 'node:os';
import path from 'node:path';
import { afterEach, test, vi } from 'vitest';
Expand Down
5 changes: 3 additions & 2 deletions packages/host-kit/src/internal/archive-extraction.fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { promises as fs } from 'node:fs';
import os from 'node:os';

import path from 'node:path';
import { gzipSync } from 'node:zlib';
import * as tar from 'tar-stream';
import { runCmdSync } from './exec.ts';
import { mkdtempForTest } from './tmp-dir.fixtures.ts';

export async function createArchiveWorkspace(): Promise<{
archivePath: string;
outputRoot: string;
root: string;
}> {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-device-archive-'));
const root = await mkdtempForTest('agent-device-archive-');
const outputRoot = path.join(root, 'output');
await fs.mkdir(outputRoot);
return { archivePath: path.join(root, 'fixture.archive'), outputRoot, root };
Expand Down
5 changes: 3 additions & 2 deletions packages/host-kit/src/internal/durable-file.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';

import path from 'node:path';
import { afterEach, expect, test, vi } from 'vitest';
import { isAtomicPublishTemporaryPath, publishDurableFileSync } from './atomic-file.ts';
import { mkdtempForTestSync } from './tmp-dir.fixtures.ts';

const roots: string[] = [];

Expand Down Expand Up @@ -109,7 +110,7 @@ test('preserves a file fsync error when descriptor cleanup also fails', () => {
});

function fixtureRoot(label: string): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), `agent-device-durable-file-${label}-`));
const root = mkdtempForTestSync(`agent-device-durable-file-${label}-`);
roots.push(root);
return root;
}
Expand Down
8 changes: 6 additions & 2 deletions packages/host-kit/src/internal/owner-identity.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';

import path from 'node:path';
import { afterEach, test, vi } from 'vitest';
import {
Expand All @@ -10,6 +10,7 @@ import {
type OwnerIdentity,
} from './owner-identity.ts';
import { readProcessStartTime } from './host-process.ts';
import { mkdtempForTestSync } from './tmp-dir.fixtures.ts';

afterEach(() => {
vi.restoreAllMocks();
Expand All @@ -28,7 +29,10 @@ test('distinguishes dead and PID-reused owners', () => {

test('distinguishes a gone state directory from permission and transient I/O failures', () => {
const startTime = readProcessStartTime(process.pid);
const missing = path.join(os.tmpdir(), `agent-device-missing-owner-${Date.now()}`);
const missing = path.join(
mkdtempForTestSync('agent-device-missing-owner'),
`agent-device-missing-owner-${Date.now()}`,
);
assert.equal(
classifyOwnerLiveness({ owner: { pid: process.pid, startTime }, stateDir: missing }),
'owner-state-dir-gone',
Expand Down
5 changes: 3 additions & 2 deletions packages/host-kit/src/internal/verified-file.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';

import path from 'node:path';
import { afterEach, expect, test, vi } from 'vitest';
import { AppError, normalizeError } from '@agent-device/kernel/errors';
Expand All @@ -17,6 +17,7 @@ import {
openVerifiedFileForRead,
openVerifiedFileForTruncate,
} from './verified-file.ts';
import { mkdtempForTestSync } from './tmp-dir.fixtures.ts';

const roots: string[] = [];

Expand Down Expand Up @@ -111,7 +112,7 @@ test('returns absent for a missing read without creating the file', () => {
});

function fixturePath(label: string): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), `agent-device-verified-${label}-`));
const root = mkdtempForTestSync(`agent-device-verified-${label}-`);
roots.push(root);
return path.join(root, 'artifact');
}
Expand Down
20 changes: 15 additions & 5 deletions packages/platform-android/src/__tests__/app-deployment.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { test } from 'vitest';
import assert from 'node:assert/strict';
import { promises as fs } from 'node:fs';
import os from 'node:os';

import path from 'node:path';
import { AppError } from '@agent-device/kernel/errors';
import { installAndroidInstallablePath } from '../app-deployment.ts';
Expand All @@ -14,7 +14,8 @@ import { withAndroidAdbProvider } from '../adb-executor.ts';
import type { DeviceInfo } from '@agent-device/kernel/device';
import { assertRejectsAppError } from './test-utils/app-error.ts';
import { withFakeAdb } from './test-utils/fake-adb.ts';
import { mkdtempForTest } from './test-utils/tmp-dir.ts';

import { mkdtempForTest, mkdtempForTestSync } from './test-utils/tmp-dir.ts';

// The fake adb provider installs through the production withAndroidAdbProvider
// scope, so `calls` records device-scoped args without a leading `-s <serial>`.
Expand All @@ -32,7 +33,10 @@ test('inferAndroidAppName derives readable names from package ids', () => {
});

test('installAndroidInstallablePath installs .apk via adb install -r', async () => {
const apkPath = path.join(os.tmpdir(), `agent-device-test-${Date.now()}.apk`);
const apkPath = path.join(
mkdtempForTestSync('agent-device-test'),
`agent-device-test-${Date.now()}.apk`,
);
await fs.writeFile(apkPath, 'placeholder', 'utf8');
await withFakeAdb(
() => undefined,
Expand All @@ -49,7 +53,10 @@ test('installAndroidInstallablePath installs .apk via adb install -r', async ()
});

test('installAndroidInstallablePath uses provider install capability when available', async () => {
const apkPath = path.join(os.tmpdir(), `agent-device-provider-install-${Date.now()}.apk`);
const apkPath = path.join(
mkdtempForTestSync('agent-device-provider-install'),
`agent-device-provider-install-${Date.now()}.apk`,
);
await fs.writeFile(apkPath, 'placeholder', 'utf8');
const installCalls: Array<{ source: string; replace: boolean | undefined }> = [];
const device: DeviceInfo = {
Expand Down Expand Up @@ -84,7 +91,10 @@ test('installAndroidInstallablePath uses provider install capability when availa
test('an app install timeout keeps the generic adb-server advice', async () => {
// The OEM install dialog can hold an app install too, but the helper-specific advice names an
// agent-device helper package, so it must not leak onto the app-under-test install path.
const apkPath = path.join(os.tmpdir(), `agent-device-app-install-timeout-${Date.now()}.apk`);
const apkPath = path.join(
mkdtempForTestSync('agent-device-app-install-timeout'),
`agent-device-app-install-timeout-${Date.now()}.apk`,
);
await fs.writeFile(apkPath, 'placeholder', 'utf8');
const device: DeviceInfo = {
platform: 'android',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import dns from 'node:dns/promises';
import { promises as fs } from 'node:fs';
// oxlint-disable-next-line no-restricted-imports -- vi.spyOn(os,'tmpdir') aims production's install scratch
import os from 'node:os';
import path from 'node:path';
import { Readable } from 'node:stream';
Expand Down
Loading
Loading