diff --git a/test/adapters/claude-session-swap.test.ts b/test/adapters/claude-session-swap.test.ts index 2768edb..bedf229 100644 --- a/test/adapters/claude-session-swap.test.ts +++ b/test/adapters/claude-session-swap.test.ts @@ -7,7 +7,7 @@ // the command that caused it. import test from 'node:test' import assert from 'node:assert/strict' -import { mkdtempSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs' +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { readRawCredential, swapCredential } from '../../src/adapters/claude-session/swap.ts' @@ -169,20 +169,29 @@ test('a source with no login is refused, and nothing is written', async () => { }) test('an unwritable target is refused, and says nothing changed', async () => { + // A read-only PARENT, so the target cannot be created inside it. Not + // `/proc/nonexistent/...`: that path throws ENOENT on macOS but makes + // `mkdirSync(recursive)` HANG on Linux, which wedged the whole suite in CI + // the first time this branch met a Linux runner. const from = fresh() const fake = fakeSecurity({ [keychainService(from, env)]: BLOB }) - // A path whose parent cannot be created. - const result = swapCredential(from, '/proc/nonexistent/swisscode-target', { - env, - platform: 'darwin', - exec: fake.exec, - }) - assert.equal(result.ok, false) - assert.equal( - fake.calls.filter((c) => c.args[0] === 'delete-generic-password').length, - 0, - 'a failed write must never drop the target\'s existing login', - ) + const parent = fresh() + chmodSync(parent, 0o500) + try { + const result = swapCredential(from, join(parent, 'target'), { + env, + platform: 'darwin', + exec: fake.exec, + }) + assert.equal(result.ok, false) + assert.equal( + fake.calls.filter((c) => c.args[0] === 'delete-generic-password').length, + 0, + "a failed write must never drop the target's existing login", + ) + } finally { + chmodSync(parent, 0o700) + } }) test('the identity block is COPIED, or the swap is only half done', async () => { diff --git a/test/adapters/fs-usage-store.test.ts b/test/adapters/fs-usage-store.test.ts index 8cee4e8..220ff52 100644 --- a/test/adapters/fs-usage-store.test.ts +++ b/test/adapters/fs-usage-store.test.ts @@ -5,7 +5,7 @@ // fail the command that triggered it. import test from 'node:test' import assert from 'node:assert/strict' -import { mkdtempSync, readFileSync, statSync, writeFileSync } from 'node:fs' +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { SNAPSHOT_TTL_MS, createFsUsageStore } from '../../src/adapters/store/fs-usage-store.ts' @@ -87,9 +87,25 @@ test('a snapshot with nothing usable left in it reads as absent', () => { }) test('an unwritable directory does not throw — the measurement already happened', () => { - const store = createFsUsageStore({ dir: '/proc/nonexistent/swisscode' }) - assert.doesNotThrow(() => store.write({ remaining: { a: 1 }, checkedAt: Date.now() })) - assert.equal(store.read(), null) + // A REAL read-only directory, matching fs-cursor-store.test.ts. + // + // The first version of this used `/proc/nonexistent/...` as a path that + // "obviously cannot be written". On macOS there is no /proc, so it failed + // fast with ENOENT and the test passed. ON LINUX `mkdirSync(recursive)` + // AGAINST THAT PATH HANGS — not throws, hangs, with no output and no exit — + // which wedged the whole suite in CI. Portable fixtures only. + const parent = fresh() + const dir = join(parent, 'state') + mkdirSync(dir) + chmodSync(dir, 0o500) + const store = createFsUsageStore({ dir }) + try { + assert.doesNotThrow(() => store.write({ remaining: { a: 1 }, checkedAt: Date.now() })) + assert.equal(store.read(), null) + } finally { + // Restored so the temp tree can be cleaned up by whoever cleans /tmp. + chmodSync(dir, 0o700) + } }) test('the file holds no credential — only account names and figures', () => {