Files
callstack__agent-device/scripts/vitest-tmpdir-global-setup.test.ts
Michał Pierzchała e832325e87 refactor(substrate): split host mechanics into @agent-device/host-kit capability ports (#2088)
* 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>
2026-08-28 07:46:48 +02:00

116 lines
4.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 owners run directory');
} finally {
fs.rmSync(abandoned, { recursive: true, force: true });
fs.rmSync(live, { recursive: true, force: true });
fs.rmSync(probePath, { force: true });
}
});