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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@
"@types/pngjs": "^6.0.5",
"@types/tar-stream": "^3.1.4",
"@types/yauzl": "^3.4.0",
"@vitest/coverage-v8": "4.1.11",
"@vitest/coverage-v8": "5.0.4",
"ai": "^7.0.68",
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-perfectionist": "^5.10.1",
Expand All @@ -347,7 +347,7 @@
"typescript": "^7.0.2",
"undici": "7.29.1",
"vite": "^8.2.1",
"vitest": "^4.1.11",
"vitest": "5.0.4",
"yaml": "^2.9.0",
"yauzl": "^3.4.0",
"@agent-device/testmu": "workspace:*"
Expand Down
192 changes: 104 additions & 88 deletions packages/host-kit/src/internal/exec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,34 +103,38 @@ test('runCmd writes stdin through pipeline', async () => {
assert.equal(result.stdout, String(stdin.length));
});

test.sequential('runCmdBackground emits bounded exec_command diagnostics when AGENT_DEVICE_EXEC_TRACE is enabled', async () => {
const diagnosticsPath = await withExecTraceEnv(
async () =>
await withDiagnosticsScope(
{
session: 'exec-trace',
requestId: 'exec-trace-1',
command: 'background',
},
async () => {
const { wait } = runCmdBackground(process.execPath, [
'-e',
'process.stdout.write("ok")',
'a',
'b',
'c',
'd',
'e',
'f',
]);
await wait;
return flushDiagnosticsToSessionFile()?.path ?? null;
},
),
);
test(
'runCmdBackground emits bounded exec_command diagnostics when AGENT_DEVICE_EXEC_TRACE is enabled',
{ concurrent: false },
async () => {
const diagnosticsPath = await withExecTraceEnv(
async () =>
await withDiagnosticsScope(
{
session: 'exec-trace',
requestId: 'exec-trace-1',
command: 'background',
},
async () => {
const { wait } = runCmdBackground(process.execPath, [
'-e',
'process.stdout.write("ok")',
'a',
'b',
'c',
'd',
'e',
'f',
]);
await wait;
return flushDiagnosticsToSessionFile()?.path ?? null;
},
),
);

assertBackgroundExecTraceEvents(readExecDiagnosticEvents(diagnosticsPath));
});
assertBackgroundExecTraceEvents(readExecDiagnosticEvents(diagnosticsPath));
},
);

test('runCmdBackground can leave output streams to the caller', async () => {
const { child, wait } = runCmdBackground(
Expand All @@ -157,7 +161,7 @@ test('runCmdBackground can leave output streams to the caller', async () => {
assert.equal(stderr, 'err');
});

test.sequential('runCmd stays silent when exec tracing is not enabled', async () => {
test('runCmd stays silent when exec tracing is not enabled', { concurrent: false }, async () => {
const previousTraceEnv = process.env.AGENT_DEVICE_EXEC_TRACE;
delete process.env.AGENT_DEVICE_EXEC_TRACE;

Expand Down Expand Up @@ -281,21 +285,25 @@ test('whichCmd rejects suspicious command strings', async () => {
assert.equal(await whichCmd('./node'), false);
});

test.sequential('whichCmd ignores directories that match a command name in PATH', async () => {
const root = mkdtempForTestSync('agent-device-whichcmd-');
const fakeCommandDir = path.join(root, 'fake-tool');
fs.mkdirSync(fakeCommandDir);
test(
'whichCmd ignores directories that match a command name in PATH',
{ concurrent: false },
async () => {
const root = mkdtempForTestSync('agent-device-whichcmd-');
const fakeCommandDir = path.join(root, 'fake-tool');
fs.mkdirSync(fakeCommandDir);

const previousPath = process.env.PATH;
process.env.PATH = `${root}${path.delimiter}${previousPath ?? ''}`;
const previousPath = process.env.PATH;
process.env.PATH = `${root}${path.delimiter}${previousPath ?? ''}`;

try {
assert.equal(await whichCmd('fake-tool'), false);
} finally {
process.env.PATH = previousPath;
fs.rmSync(root, { recursive: true, force: true });
}
});
try {
assert.equal(await whichCmd('fake-tool'), false);
} finally {
process.env.PATH = previousPath;
fs.rmSync(root, { recursive: true, force: true });
}
},
);

function readExecDiagnosticEvents(diagnosticsPath: string | null): Array<{
level?: string;
Expand Down Expand Up @@ -542,55 +550,63 @@ test('an allowFailure xcrun result guarded by requireExecSuccess gets the toolch
);
});

test.sequential('without a developer dir the toolchain hint points at xcode-select', () => {
const saved = process.env.DEVELOPER_DIR;
delete process.env.DEVELOPER_DIR;
try {
// No cmd/args: the call site only had the result, so xcrun's own stderr identifies the tool.
assert.throws(
() => requireExecSuccess(XCRUN_DEVICECTL_NOT_FOUND, 'Failed to list devices'),
(error: unknown) => {
assert.ok(error instanceof AppError);
assert.equal(error.details?.developerDir, null);
assert.match(
String(error.details?.hint),
/devicectl in the developer dir selected by xcode-select/,
);
return true;
},
);
} finally {
if (saved !== undefined) process.env.DEVELOPER_DIR = saved;
}
});
test(
'without a developer dir the toolchain hint points at xcode-select',
{ concurrent: false },
() => {
const saved = process.env.DEVELOPER_DIR;
delete process.env.DEVELOPER_DIR;
try {
// No cmd/args: the call site only had the result, so xcrun's own stderr identifies the tool.
assert.throws(
() => requireExecSuccess(XCRUN_DEVICECTL_NOT_FOUND, 'Failed to list devices'),
(error: unknown) => {
assert.ok(error instanceof AppError);
assert.equal(error.details?.developerDir, null);
assert.match(
String(error.details?.hint),
/devicectl in the developer dir selected by xcode-select/,
);
return true;
},
);
} finally {
if (saved !== undefined) process.env.DEVELOPER_DIR = saved;
}
},
);

test.sequential('a request that clears DEVELOPER_DIR is not reported under the daemon one', async () => {
const saved = process.env.DEVELOPER_DIR;
process.env.DEVELOPER_DIR = '/daemon/Developer';
try {
// The child runs without a usable DEVELOPER_DIR (xcode-select decides), so the key and report
// must too, whether the request clears it or carries it unset.
for (const requestEnv of [{ DEVELOPER_DIR: '' }, { DEVELOPER_DIR: undefined }]) {
await withRequestCommandEnv(requestEnv, async () => {
assert.equal((await runCmd(process.execPath, PRINT_DEVELOPER_DIR)).stdout, '');
assert.equal(commandDeveloperDir(), undefined);
assert.throws(
() => requireExecSuccess(XCRUN_DEVICECTL_NOT_FOUND, 'Failed to list devices'),
(error: unknown) => {
assert.ok(error instanceof AppError);
assert.equal(error.details?.developerDir, null);
assert.match(String(error.details?.hint), /selected by xcode-select/);
return true;
},
);
});
test(
'a request that clears DEVELOPER_DIR is not reported under the daemon one',
{ concurrent: false },
async () => {
const saved = process.env.DEVELOPER_DIR;
process.env.DEVELOPER_DIR = '/daemon/Developer';
try {
// The child runs without a usable DEVELOPER_DIR (xcode-select decides), so the key and report
// must too, whether the request clears it or carries it unset.
for (const requestEnv of [{ DEVELOPER_DIR: '' }, { DEVELOPER_DIR: undefined }]) {
await withRequestCommandEnv(requestEnv, async () => {
assert.equal((await runCmd(process.execPath, PRINT_DEVELOPER_DIR)).stdout, '');
assert.equal(commandDeveloperDir(), undefined);
assert.throws(
() => requireExecSuccess(XCRUN_DEVICECTL_NOT_FOUND, 'Failed to list devices'),
(error: unknown) => {
assert.ok(error instanceof AppError);
assert.equal(error.details?.developerDir, null);
assert.match(String(error.details?.hint), /selected by xcode-select/);
return true;
},
);
});
}
assert.equal(commandDeveloperDir(), '/daemon/Developer');
} finally {
if (saved === undefined) delete process.env.DEVELOPER_DIR;
else process.env.DEVELOPER_DIR = saved;
}
assert.equal(commandDeveloperDir(), '/daemon/Developer');
} finally {
if (saved === undefined) delete process.env.DEVELOPER_DIR;
else process.env.DEVELOPER_DIR = saved;
}
});
},
);

test('a non-xcrun failure keeps the caller hint', () => {
assert.throws(
Expand Down
4 changes: 2 additions & 2 deletions packages/kernel/src/ttl-memo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { createTtlMemo } from './ttl-memo.ts';

const crossTestMemo = createTtlMemo<string, string>({ ttlMs: 60_000 });

test.sequential('process memo setup clears values after each test: seed', () => {
test('process memo setup clears values after each test: seed', { concurrent: false }, () => {
crossTestMemo.set('key', 'value');

expect(crossTestMemo.get('key')).toBe('value');
});

test.sequential('process memo setup clears values after each test: verify', () => {
test('process memo setup clears values after each test: verify', { concurrent: false }, () => {
expect(crossTestMemo.get('key')).toBeUndefined();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,24 @@ test('booted memo is scoped per simulator set path', async () => {
expect(countSimctlListCalls()).toBe(2);
});

test.sequential('process memo test setup clears simulator boot memo: seed', async () => {
markSimulatorBooted(simulator);

await ensureBootedSimulator(simulator);

expect(countSimctlListCalls()).toBe(0);
});

test.sequential('process memo test setup clears simulator boot memo: verify', async () => {
await ensureBootedSimulator(simulator);

expect(countSimctlListCalls()).toBe(1);
});
test(
'process memo test setup clears simulator boot memo: seed',
{ concurrent: false },
async () => {
markSimulatorBooted(simulator);

await ensureBootedSimulator(simulator);

expect(countSimctlListCalls()).toBe(0);
},
);

test(
'process memo test setup clears simulator boot memo: verify',
{ concurrent: false },
async () => {
await ensureBootedSimulator(simulator);

expect(countSimctlListCalls()).toBe(1);
},
);
9 changes: 8 additions & 1 deletion packages/test-kit/src/legacy-daemon-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,14 @@ const acquired = acquireDaemonLock(paths.baseDir, paths.lockPath, {
});
fs.writeFileSync(paths.baseDir + '/legacy-disposition.json', JSON.stringify({ acquired }));
if (!acquired) process.exit(0);
fs.writeFileSync(paths.infoPath, JSON.stringify({ ...identity, port: 4210, token: 'legacy-token', version: '0.21.20' }));
const registration = JSON.stringify({ ...identity, port: 4210, token: 'legacy-token', version: '0.21.20' });
// Published through a temp file like the real owner does: callers wait on
// fs.existsSync(paths.infoPath) as the signal that registration completed, and a direct
// writeFileSync creates the file before its bytes exist, so that wait can return an
// empty registration. Renaming last also means the lock file is fully written by then.
const registrationTemp = paths.infoPath + '.' + process.pid + '.tmp';
fs.writeFileSync(registrationTemp, registration);
fs.renameSync(registrationTemp, paths.infoPath);
process.on('SIGTERM', () => { releaseDaemonLock(paths.lockPath); process.exit(0); });
setInterval(() => {}, 1000);
`,
Expand Down
Loading
Loading