Files
callstack__agent-device/scripts/__tests__/fallow-fixture-policy.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

114 lines
3.8 KiB
TypeScript

import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { test } from 'vitest';
import { mkdtempForTest } from '../../src/__tests__/test-utils/tmp-dir.ts';
import { runCmd } from '@agent-device/host-kit/command';
const REPOSITORY_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const FALLOW_CLI = path.join(REPOSITORY_ROOT, 'node_modules', 'fallow', 'bin', 'fallow');
const FIXTURE_GLOB = '**/*.fixtures.ts';
type IgnoreExportPolicy = {
file: string;
exports: string[];
};
type ResolvedFallowConfig = {
ignorePatterns: string[];
ignoreExports: IgnoreExportPolicy[];
};
type DeadCodeReport = {
summary: { unused_exports: number };
unused_exports: Array<{ path: string; export_name: string }>;
};
type HealthReport = {
findings: Array<{ path: string; name: string }>;
};
function parseJsonEnvelope<T>(stdout: string): T {
const jsonStart = stdout.indexOf('{');
assert.notEqual(jsonStart, -1, `expected JSON output, received: ${stdout}`);
return JSON.parse(stdout.slice(jsonStart)) as T;
}
async function runFallow(root: string, args: string[]) {
return await runCmd(process.execPath, [FALLOW_CLI, ...args], {
cwd: root,
allowFailure: true,
timeoutMs: 30_000,
});
}
test('fixture exports stay exempt while fixture health remains audited', async () => {
const configResult = await runFallow(REPOSITORY_ROOT, ['config', '--format', 'json', '--quiet']);
assert.equal(configResult.exitCode, 0, configResult.stderr);
const resolved = parseJsonEnvelope<ResolvedFallowConfig>(configResult.stdout);
const fixturePolicy = resolved.ignoreExports.find(({ file }) => file === FIXTURE_GLOB);
const root = await mkdtempForTest('fallow-fixture-policy-');
await fs.mkdir(path.join(root, 'src'));
await Promise.all([
fs.writeFile(path.join(root, 'package.json'), '{"type":"module"}\n'),
fs.writeFile(
path.join(root, 'tsconfig.json'),
'{"compilerOptions":{"target":"ES2022","module":"NodeNext","moduleResolution":"NodeNext"},"include":["src/**/*.ts"]}\n',
),
fs.writeFile(
path.join(root, 'src', 'index.ts'),
"import './sample.fixtures.ts';\nexport const entry = true;\n",
),
fs.writeFile(
path.join(root, 'src', 'sample.fixtures.ts'),
'export function unusedFixture(value: boolean): number {\n if (value) return 1;\n return 0;\n}\n',
),
fs.writeFile(
path.join(root, '.fallowrc.json'),
`${JSON.stringify({
entry: ['src/index.ts'],
ignorePatterns: resolved.ignorePatterns,
ignoreExports: fixturePolicy ? [fixturePolicy] : [],
health: { maxCyclomatic: 1, maxCognitive: 1 },
})}\n`,
),
]);
const deadCodeResult = await runFallow(root, [
'dead-code',
'--unused-exports',
'--format',
'json',
'--quiet',
'--no-cache',
]);
assert.equal(deadCodeResult.exitCode, 0, deadCodeResult.stderr);
const deadCode = parseJsonEnvelope<DeadCodeReport>(deadCodeResult.stdout);
assert.equal(deadCode.summary.unused_exports, 0);
assert.deepEqual(deadCode.unused_exports, []);
const healthResult = await runFallow(root, [
'health',
'--complexity',
'--format',
'json',
'--quiet',
'--no-cache',
]);
assert.equal(healthResult.exitCode, 1, 'the planted fixture complexity must fail health');
const health = parseJsonEnvelope<HealthReport>(healthResult.stdout);
assert.ok(
health.findings.some(
({ path: findingPath, name }) =>
findingPath === 'src/sample.fixtures.ts' && name === 'unusedFixture',
),
'Fallow health did not analyze the planted fixture',
);
assert.equal(resolved.ignorePatterns.includes(FIXTURE_GLOB), false);
assert.ok(fixturePolicy, `missing ${FIXTURE_GLOB} unused-export policy`);
assert.deepEqual(fixturePolicy.exports, ['*']);
});