mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
e832325e87
* refactor: split generic host mechanics into @agent-device/host-kit (#2082 W1) The shared src/utils closure that blocked the platform-family moves lands on declared owners: generic host mechanics form a new private @agent-device/host-kit package between kernel and capture-kit, and capture-kit keeps capture, snapshot, and recording behavior, depending on host-kit for the mechanics it needs. tar-stream and yauzl move with the archive code. Every seam's exported subpaths are pinned in package-boundaries.test.ts, the layering model ranks the new zone, R13's allow-list names it, and each seam carries an exact eager-closure row. ADR-0019's substrate amendment describes the layout. Tests that mocked two of the moved modules separately became duplicate same-seam vi.mock factories, where the second silently replaced the first; those are merged, and the mocks that production code reaches past are pinned at their injection points instead. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * refactor(host-kit): one narrow capability port per export The four technical barrels (exec/fs/values/request) grouped by category rather than by capability, so a consumer needing one mechanic evaluated unrelated ones. Each export is now a single capability over the host machine: command, process, diagnostics, retry, archive, file, request, version. A port re-exports only what a consumer of that capability uses, and every port carries its own eager-closure row. Most of the old values barrel was never host mechanics. Pure record readers, config-source values, result text, memoization, async scoping, coordinate validation, and device-scope parsing touch no process, file, or environment, so they join kernel's other primitives instead. Closures fall accordingly: capture-kit's png-worker-client from 20 to 10, png-resize from 28 to 18, session-teardown from 79 to 68, and the CLI from 386 to 380. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * chore: drop the migration inventories and trim the touched comments Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * docs: trim the touched host-kit and mutation-lane comments Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * docs: keep tool directives only in the touched files Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * docs: keep tool directives only across the touched tree Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * fix: point the Swift parity comment at the real TS twin and test The W1 move rewrote this citation to packages/contracts/src/mobile-snapshot-semantics.ts, which does not exist: the module went to capture-kit while isTapPointInsideViewport itself went to packages/contracts/src/snapshot-visibility.ts. The TS test line was left pointing at the pre-move path. Both now resolve. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * fix: repoint comment citations at the homes this refactor moved them to The W1 move left ~20 comment citations pointing at src/utils/*.ts and src/request/*.ts paths that no longer exist. Each now names the capability port that owns the symbol, which survives further file moves: exec -> host-kit/command host-process, owner-identity -> host-kit/process diagnostics -> host-kit/diagnostics atomic-file, process-lock -> host-kit/file retry -> host-kit/retry request progress/cancel -> host-kit/request version -> host-kit/version ttl-memo, source-value, parsing, device-isolation, keyed-lock, success-text -> kernel subpaths Comment-only; no closure, budget, or behavior change. ADR citations are left as written, being dated records of the decision rather than live references. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH --------- Co-authored-by: Claude <noreply@anthropic.com>
116 lines
4.6 KiB
TypeScript
116 lines
4.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
||
import crypto from 'node:crypto';
|
||
import fs from 'node:fs';
|
||
import os from 'node:os';
|
||
import path from 'node:path';
|
||
import { test } from 'node:test';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { runCmd } from '@agent-device/host-kit/command';
|
||
import { TEST_RUN_TMP_PREFIX, TEST_RUN_TMP_ROOT } from './check-tmpdir-leaks-model.ts';
|
||
|
||
const REPOSITORY_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||
|
||
test('the configured Vitest lifecycle redirects worker TMPDIR and removes it after the run', async () => {
|
||
const probeName = `vitest-tmpdir-probe-${process.pid}-${Date.now()}.test.ts`;
|
||
const probePath = path.join(REPOSITORY_ROOT, 'src', '__tests__', probeName);
|
||
const evidenceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'vitest-tmpdir-lifecycle-test-'));
|
||
const evidencePath = path.join(evidenceRoot, 'worker-tmpdir.txt');
|
||
const expectedSwiftCacheDir = path.join(os.tmpdir(), 'agent-device-swift-cache');
|
||
let workerTmpDir: string | undefined;
|
||
|
||
fs.writeFileSync(
|
||
probePath,
|
||
`import fs from 'node:fs';
|
||
import os from 'node:os';
|
||
import path from 'node:path';
|
||
import { expect, test } from 'vitest';
|
||
|
||
test('worker inherits the run-owned temp directory', () => {
|
||
const tmpdir = os.tmpdir();
|
||
expect(path.dirname(tmpdir)).toBe(${JSON.stringify(TEST_RUN_TMP_ROOT)});
|
||
expect(path.basename(tmpdir)).toMatch(new RegExp(${JSON.stringify(
|
||
`^${TEST_RUN_TMP_PREFIX}\\d+-`,
|
||
)}));
|
||
expect(process.env.AGENT_DEVICE_SWIFT_CACHE_DIR).toBe(${JSON.stringify(expectedSwiftCacheDir)});
|
||
expect(process.env.AGENT_DEVICE_SWIFT_CACHE_DIR?.startsWith(tmpdir)).toBe(false);
|
||
fs.writeFileSync(process.env.AGENT_DEVICE_TEST_TMPDIR_EVIDENCE_PATH!, tmpdir);
|
||
});
|
||
`,
|
||
);
|
||
|
||
try {
|
||
await runCmd(
|
||
path.join(REPOSITORY_ROOT, 'node_modules', '.bin', 'vitest'),
|
||
['run', '--project', 'unit-core', probePath],
|
||
{
|
||
cwd: REPOSITORY_ROOT,
|
||
env: {
|
||
...process.env,
|
||
AGENT_DEVICE_SWIFT_CACHE_DIR: '',
|
||
AGENT_DEVICE_TEST_TMPDIR_EVIDENCE_PATH: evidencePath,
|
||
},
|
||
timeoutMs: 30_000,
|
||
},
|
||
);
|
||
|
||
workerTmpDir = fs.readFileSync(evidencePath, 'utf8');
|
||
assert.equal(path.dirname(workerTmpDir), TEST_RUN_TMP_ROOT);
|
||
assert.match(path.basename(workerTmpDir), new RegExp(`^${TEST_RUN_TMP_PREFIX}\\d+-`));
|
||
assert.equal(
|
||
fs.existsSync(workerTmpDir),
|
||
false,
|
||
'global teardown must remove the run directory',
|
||
);
|
||
} finally {
|
||
if (workerTmpDir) fs.rmSync(workerTmpDir, { recursive: true, force: true });
|
||
fs.rmSync(probePath, { force: true });
|
||
fs.rmSync(evidenceRoot, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
// INT32_MAX exceeds every platform's pid range (Linux pid_max caps at 2^22,
|
||
// macOS at 99999), so kill(pid, 0) is ESRCH by construction — an owner that
|
||
// is dead and can never be reused mid-test, unlike a freshly exited child's pid.
|
||
const NEVER_A_PID = 2_147_483_647;
|
||
|
||
test('global setup prunes a run directory abandoned by an earlier killed run and keeps a live one', async () => {
|
||
const stamp = crypto.randomUUID();
|
||
const abandoned = path.join(
|
||
TEST_RUN_TMP_ROOT,
|
||
`${TEST_RUN_TMP_PREFIX}${NEVER_A_PID}-planted-${stamp}`,
|
||
);
|
||
// Owned by this test process, which is alive for the whole nested run: the
|
||
// same shape as a concurrent run in another worktree, and must survive.
|
||
const live = path.join(
|
||
TEST_RUN_TMP_ROOT,
|
||
`${TEST_RUN_TMP_PREFIX}${process.pid}-planted-${stamp}`,
|
||
);
|
||
const probeName = `vitest-tmpdir-prune-probe-${process.pid}-${stamp}.test.ts`;
|
||
const probePath = path.join(REPOSITORY_ROOT, 'src', '__tests__', probeName);
|
||
fs.mkdirSync(path.join(abandoned, 'nested'), { recursive: true });
|
||
fs.writeFileSync(path.join(abandoned, 'nested', 'leftover.txt'), 'from a killed run');
|
||
fs.mkdirSync(live);
|
||
fs.writeFileSync(
|
||
probePath,
|
||
`import { test } from 'vitest';
|
||
|
||
test('noop probe: the run itself is the subject', () => {});
|
||
`,
|
||
);
|
||
|
||
try {
|
||
const result = await runCmd(
|
||
path.join(REPOSITORY_ROOT, 'node_modules', '.bin', 'vitest'),
|
||
['run', '--project', 'unit-core', probePath],
|
||
{ cwd: REPOSITORY_ROOT, timeoutMs: 30_000 },
|
||
);
|
||
assert.equal(result.exitCode, 0, `probe run failed:\n${result.stdout}\n${result.stderr}`);
|
||
assert.equal(fs.existsSync(abandoned), false, 'setup must prune the abandoned run directory');
|
||
assert.equal(fs.existsSync(live), true, 'setup must never touch a live owner’s run directory');
|
||
} finally {
|
||
fs.rmSync(abandoned, { recursive: true, force: true });
|
||
fs.rmSync(live, { recursive: true, force: true });
|
||
fs.rmSync(probePath, { force: true });
|
||
}
|
||
});
|