Files
trackgo-fe/.agents/skills/archify/test/update-notifier.test.mjs
T
shancheas 166e0d40ac feat: introduce archify skill for generating architecture diagrams
- Added a new Archify skill, enabling users to create polished architecture, workflow, sequence, data-flow, and lifecycle diagrams.
- Implemented comprehensive functionality including rendering, validation, and delivery of diagrams in various formats.
- Integrated a user-friendly command-line interface for generating and previewing diagrams.
- Developed supporting files including package.json, LICENSE, and SKILL.md for documentation and licensing.
- Added unit tests to ensure reliability and functionality of the new skill.

These changes enhance the application by providing a structured approach to visualizing system architecture and workflows, improving user experience and data representation.
2026-08-31 11:31:29 +07:00

3318 lines
121 KiB
JavaScript

import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import fs from 'node:fs';
import fsPromises from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import test from 'node:test';
import { fileURLToPath, pathToFileURL } from 'node:url';
import {
acknowledgeUpdate,
checkForUpdate,
} from '../scripts/check-update.mjs';
import { DEFAULT_MANIFEST_URL, compareSemver, parseSemver } from '../scripts/update-contract.mjs';
const here = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(here, '..');
const checkerPath = path.join(skillRoot, 'scripts', 'check-update.mjs');
const contractPath = path.join(skillRoot, 'scripts', 'update-contract.mjs');
const expectedRepository = 'https://github.com/tt-a1i/archify';
const expectedManifestUrl = 'https://tt-a1i.github.io/archify/skill-updates/archify/stable.json';
const baseTime = Date.parse('2026-08-28T08:00:00Z');
const childCheckTimeoutMs = 2_000;
const parentCheckTimeoutMs = 5_000;
const maxCacheStateBytes = 64 * 1_024;
function writeJson(target, value) {
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`);
}
function compactStateSource(state) {
return `${JSON.stringify(state)}\n`;
}
function metadataWithOverrides(metadata, overrides) {
return new Proxy(metadata, {
get(target, property) {
if (Object.hasOwn(overrides, property)) return overrides[property];
const value = Reflect.get(target, property, target);
return typeof value === 'function' ? value.bind(target) : value;
},
});
}
function historyDigest(index) {
return `sha256:${crypto.createHash('sha256').update(`history-${index}`).digest('hex')}`;
}
function cachedStateWithHistory({ offeredDigests = [], acknowledgedDigests = [] } = {}) {
return {
schemaVersion: 1,
skillId: 'archify',
installedVersion: '2.15.0',
check: {
nextCheckAt: '2020-01-01T00:00:00.000Z',
consecutiveFailures: 0,
},
notification: {
offeredDigests: [...offeredDigests],
acknowledgedDigests: [...acknowledgedDigests],
},
};
}
function candidateStateForDigest(state, digest) {
return {
...state,
check: {
nextCheckAt: new Date(baseTime + (72 * 60 * 60 * 1_000)).toISOString(),
consecutiveFailures: 0,
},
notification: {
offeredDigests: [...state.notification.offeredDigests, digest],
acknowledgedDigests: [...state.notification.acknowledgedDigests],
},
candidate: {
version: '2.16.0',
targetDigest: digest,
severity: 'normal',
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0',
},
};
}
function writeCompactCommittedState(testFixture, state, generation = 1n) {
const target = path.join(operationPath(testFixture, 'committed', generation), 'state.json');
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, compactStateSource(state));
return target;
}
function committedStateFiles(testFixture) {
return fs.readdirSync(stateDirectory(testFixture), { withFileTypes: true })
.filter((entry) => entry.isDirectory() && /^committed-\d+$/.test(entry.name))
.map((entry) => path.join(stateDirectory(testFixture), entry.name, 'state.json'));
}
function localRelease(version = '2.15.0') {
return {
schemaVersion: 1,
skillId: 'archify',
channel: version.includes('-') ? 'development' : 'stable',
version,
source: { repository: expectedRepository },
updateManifestUrl: expectedManifestUrl,
};
}
function remoteRelease(overrides = {}) {
return {
schemaVersion: 1,
skillId: 'archify',
channel: 'stable',
version: '2.16.0',
publishedAt: '2026-08-28T07:00:00Z',
source: {
repository: expectedRepository,
ref: 'v2.16.0',
treeSha: 'a'.repeat(40),
},
artifact: {
sha256: 'b'.repeat(64),
},
summary: 'Improve large-repository scanning and diagram layout.',
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0',
severity: 'normal',
...overrides,
};
}
function remoteReleaseForVersion(version, digest = 'b'.repeat(64)) {
const release = remoteRelease();
return {
...release,
version,
source: { ...release.source, ref: `v${version}` },
artifact: { sha256: digest },
releaseNotes: `https://github.com/tt-a1i/archify/releases/tag/v${version}`,
};
}
function response(body, { status = 200, etag = '"archify-2.16.0"' } = {}) {
return new Response(typeof body === 'string' ? body : JSON.stringify(body), {
status,
headers: {
'content-type': 'application/json',
etag,
},
});
}
function fixture(version = '2.15.0') {
const root = fs.realpathSync(fs.mkdtempSync(
path.join(os.tmpdir(), 'archify-update-notifier-'),
));
const releasePath = path.join(root, 'skill-release.json');
const cacheDirectory = path.join(root, 'cache');
writeJson(releasePath, localRelease(version));
return { root, releasePath, cacheDirectory };
}
function stateDirectory(testFixture, version = '2.15.0') {
const partition = crypto.createHash('sha256').update(version).digest('hex').slice(0, 24);
return path.join(testFixture.cacheDirectory, `version-${partition}`);
}
function statePath(testFixture, version = '2.15.0') {
const directory = stateDirectory(testFixture, version);
let committed = [];
try {
committed = fs.readdirSync(directory)
.filter((name) => /^committed-\d+$/.test(name))
.sort((left, right) => {
const leftGeneration = BigInt(left.slice('committed-'.length));
const rightGeneration = BigInt(right.slice('committed-'.length));
return leftGeneration > rightGeneration ? -1 : 1;
});
} catch {
// A seed may be written before the cache partition exists.
}
return committed[0]
? path.join(directory, committed[0], 'state.json')
: path.join(operationPath(testFixture, 'committed', 1n, version), 'state.json');
}
function operationPath(testFixture, kind, generation, version = '2.15.0') {
return path.join(
stateDirectory(testFixture, version),
`${kind}-${BigInt(generation).toString().padStart(20, '0')}`,
);
}
function writePendingOperation(testFixture, {
generation = 1n,
owner = { pid: process.pid, token: 'e'.repeat(32) },
modifiedAt = new Date(),
} = {}) {
const directory = operationPath(testFixture, 'pending', generation);
fs.mkdirSync(directory, { recursive: true });
const ownerPath = path.join(directory, 'owner.json');
fs.writeFileSync(ownerPath, typeof owner === 'string' ? owner : `${JSON.stringify(owner)}\n`);
fs.utimesSync(ownerPath, modifiedAt, modifiedAt);
return directory;
}
function pauseMkdirOnce(targetPath) {
const originalMkdir = fsPromises.mkdir;
let release;
let markReached;
let intercepted = false;
const reached = new Promise((resolve) => { markReached = resolve; });
const gate = new Promise((resolve) => { release = resolve; });
fsPromises.mkdir = async (target, ...args) => {
if (!intercepted && path.resolve(target) === path.resolve(targetPath)) {
intercepted = true;
markReached();
await gate;
}
return originalMkdir(target, ...args);
};
return {
reached,
release,
restore() {
release();
fsPromises.mkdir = originalMkdir;
},
};
}
function pauseReaddirSnapshot(targetDirectory, callNumber) {
const originalReaddir = fsPromises.readdir;
let release;
let markReached;
let calls = 0;
const reached = new Promise((resolve) => { markReached = resolve; });
const gate = new Promise((resolve) => { release = resolve; });
fsPromises.readdir = async (target, ...args) => {
const result = await originalReaddir(target, ...args);
if (path.resolve(target) === path.resolve(targetDirectory) && ++calls === callNumber) {
markReached();
await gate;
}
return result;
};
return {
reached,
release,
restore() {
release();
fsPromises.readdir = originalReaddir;
},
};
}
function pauseReaddirSnapshotOnce(targetDirectory, predicate) {
const originalReaddir = fsPromises.readdir;
let release;
let markReached;
let intercepted = false;
const reached = new Promise((resolve) => { markReached = resolve; });
const gate = new Promise((resolve) => { release = resolve; });
fsPromises.readdir = async (target, ...args) => {
const result = await originalReaddir(target, ...args);
if (!intercepted
&& path.resolve(target) === path.resolve(targetDirectory)
&& predicate(result)) {
intercepted = true;
markReached();
await gate;
}
return result;
};
return {
reached,
release,
restore() {
release();
fsPromises.readdir = originalReaddir;
},
};
}
function pauseOpenOnce(predicate) {
const originalOpen = fsPromises.open;
let release;
let markReached;
let intercepted = false;
const reached = new Promise((resolve) => { markReached = resolve; });
const gate = new Promise((resolve) => { release = resolve; });
fsPromises.open = async (target, ...args) => {
if (!intercepted && predicate(target)) {
intercepted = true;
markReached();
await gate;
}
return originalOpen(target, ...args);
};
return {
reached,
release,
restore() {
release();
fsPromises.open = originalOpen;
},
};
}
function pauseWriteOnce(basename) {
return pauseOpenOnce((target) => path.basename(target) === basename);
}
function pauseRenameMatchingOnce(predicate) {
const originalRename = fsPromises.rename;
let release;
let markReached;
let intercepted = false;
const reached = new Promise((resolve) => { markReached = resolve; });
const gate = new Promise((resolve) => { release = resolve; });
fsPromises.rename = async (source, destination, ...args) => {
if (!intercepted && predicate(source, destination)) {
intercepted = true;
markReached();
await gate;
}
return originalRename(source, destination, ...args);
};
return {
reached,
release,
restore() {
release();
fsPromises.rename = originalRename;
},
};
}
function pauseRenameOnce(sourcePath, destinationPath) {
return pauseRenameMatchingOnce((source, destination) => (
path.resolve(source) === path.resolve(sourcePath)
&& path.resolve(destination) === path.resolve(destinationPath)
));
}
function options(testFixture, fetchImpl, overrides = {}) {
return {
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
fetchImpl,
now: () => baseTime,
random: () => 0.5,
timeoutMs: 50,
...overrides,
};
}
function cachedUpdateState() {
return {
schemaVersion: 1,
skillId: 'archify',
installedVersion: '2.15.0',
check: {
nextCheckAt: new Date(baseTime + (24 * 60 * 60 * 1_000)).toISOString(),
consecutiveFailures: 0,
},
notification: {
offeredDigests: [`sha256:${'b'.repeat(64)}`],
acknowledgedDigests: [],
},
candidate: {
version: '2.16.0',
targetDigest: `sha256:${'b'.repeat(64)}`,
severity: 'normal',
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0',
},
};
}
function createFifo(target) {
const created = spawnSync('mkfifo', [target], { encoding: 'utf8' });
if (created.error?.code === 'ENOENT') return false;
assert.equal(created.status, 0, created.stderr || created.error?.message);
return true;
}
function runCheckInChild(testFixture) {
const manifest = remoteReleaseForVersion('2.15.0', 'd'.repeat(64));
const childSource = `
import { checkForUpdate } from ${JSON.stringify(pathToFileURL(checkerPath).href)};
const watchdog = setTimeout(() => {
process.stderr.write('child update check timed out\\n');
process.exit(124);
}, ${childCheckTimeoutMs});
let requests = 0;
try {
const result = await checkForUpdate({
releasePath: ${JSON.stringify(testFixture.releasePath)},
cacheDirectory: ${JSON.stringify(testFixture.cacheDirectory)},
fetchImpl: async () => {
requests += 1;
return new Response(${JSON.stringify(JSON.stringify(manifest))}, {
status: 200,
headers: { 'content-type': 'application/json' },
});
},
now: () => ${baseTime},
random: () => 0.5,
timeoutMs: 50,
});
process.stdout.write(JSON.stringify({ result, requests }));
} finally {
clearTimeout(watchdog);
}
`;
const child = spawnSync(process.execPath, ['--input-type=module', '--eval', childSource], {
cwd: skillRoot,
encoding: 'utf8',
timeout: parentCheckTimeoutMs,
});
const diagnostics = [child.error?.stack, child.stderr, child.stdout].filter(Boolean).join('\n');
assert.equal(child.error, undefined, diagnostics);
assert.equal(child.status, 0, diagnostics);
return JSON.parse(child.stdout);
}
function assertUnsafeCacheStateIsIgnored(testFixture) {
assert.deepEqual(runCheckInChild(testFixture), {
result: { status: 'silent', reason: 'current' },
requests: 1,
});
}
test('production manifest URL is a fixed trusted GitHub Pages resource', () => {
assert.equal(DEFAULT_MANIFEST_URL, expectedManifestUrl);
const local = JSON.parse(fs.readFileSync(path.join(skillRoot, 'skill-release.json'), 'utf8'));
assert.equal(local.updateManifestUrl, expectedManifestUrl);
assert.equal(local.source.repository, expectedRepository);
});
test('SemVer comparison handles stable, prerelease, and downgrade ordering', () => {
assert.equal(compareSemver('2.16.0', '2.15.0'), 1);
assert.equal(compareSemver('2.16.0-dev.0', '2.15.0'), 1);
assert.equal(compareSemver('2.16.0', '2.16.0-dev.9'), 1);
assert.equal(compareSemver('2.16.0-dev.2', '2.16.0-dev.10'), -1);
assert.equal(compareSemver('2.16.0', '2.16.0'), 0);
assert.equal(compareSemver('2.16.0+build.9', '2.16.0+build.1'), 0);
assert.equal(compareSemver('9007199254740993.0.0', '9007199254740992.0.0'), 1);
assert.equal(compareSemver('2.16.0-dev.9007199254740993', '2.16.0-dev.9007199254740992'), 1);
assert.throws(() => compareSemver('2.16.0-dev.01', '2.16.0'));
});
test('development installs never treat the older stable release as an update', async (t) => {
const testFixture = fixture('2.16.0-dev.0');
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const result = await checkForUpdate(options(
testFixture,
async () => response(remoteRelease({
version: '2.15.0',
source: {
repository: expectedRepository,
ref: 'v2.15.0',
treeSha: 'c'.repeat(40),
},
artifact: { sha256: 'd'.repeat(64) },
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.15.0',
})),
));
assert.deepEqual(result, { status: 'silent', reason: 'current' });
});
test('a changed digest never bypasses same-version or downgrade protection', async () => {
for (const [version, digest] of [
['2.15.0', 'c'.repeat(64)],
['2.14.9', 'd'.repeat(64)],
]) {
const testFixture = fixture();
try {
const result = await checkForUpdate(options(
testFixture,
async () => response(remoteReleaseForVersion(version, digest)),
));
assert.deepEqual(result, { status: 'silent', reason: 'current' });
} finally {
fs.rmSync(testFixture.root, { recursive: true, force: true });
}
}
});
test('a successful refresh withdraws a previously offered higher candidate', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
let requests = 0;
const fetchImpl = async () => {
requests += 1;
return response(requests === 1
? remoteReleaseForVersion('3.0.0', 'c'.repeat(64))
: remoteReleaseForVersion('2.15.0', 'd'.repeat(64)));
};
const offered = await checkForUpdate(options(testFixture, fetchImpl));
assert.equal(offered.status, 'update_available');
assert.equal(offered.latestVersion, '3.0.0');
const withdrawn = await checkForUpdate(options(testFixture, fetchImpl, {
now: () => baseTime + (73 * 60 * 60 * 1_000),
}));
assert.deepEqual(withdrawn, { status: 'silent', reason: 'current' });
assert.equal(JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).candidate.version, '2.15.0');
assert.equal(requests, 2);
});
test('a newer immutable candidate is re-offered until the visible notice is acknowledged', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
let requests = 0;
const fetchImpl = async () => {
requests += 1;
return response(remoteRelease());
};
const first = await checkForUpdate(options(testFixture, fetchImpl));
assert.equal(first.status, 'update_available');
assert.equal(first.installedVersion, '2.15.0');
assert.equal(first.latestVersion, '2.16.0');
assert.equal(first.eventKey, `archify@sha256:${'b'.repeat(64)}`);
assert.equal(first.targetDigest, `sha256:${'b'.repeat(64)}`);
assert.equal(Object.hasOwn(first, 'updateCommand'), false);
assert.equal(first.summary, 'Archify 2.16.0 is available; see the official release notes for details.');
const persisted = JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8'));
assert.deepEqual(Object.keys(persisted.check).sort(), [
'consecutiveFailures', 'nextCheckAt',
]);
assert.deepEqual(Object.keys(persisted.notification).sort(), [
'acknowledgedDigests', 'offeredDigests',
]);
assert.deepEqual(Object.keys(persisted.candidate).sort(), [
'releaseNotes', 'severity', 'targetDigest', 'version',
]);
const second = await checkForUpdate(options(testFixture, fetchImpl));
assert.equal(second.status, 'update_available');
assert.equal(second.eventKey, first.eventKey);
assert.equal(requests, 1, 'fresh cached candidates must not make another request');
const acknowledgement = await acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: first.eventKey,
now: () => baseTime + 1_000,
});
assert.deepEqual(acknowledgement, { status: 'acknowledged', eventKey: first.eventKey });
assert.deepEqual(
JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).notification.acknowledgedDigests,
[first.targetDigest],
);
const third = await checkForUpdate(options(testFixture, fetchImpl));
assert.deepEqual(third, { status: 'silent', reason: 'already-notified' });
assert.equal(requests, 1);
});
test('an acknowledged candidate stays suppressed after a later candidate and manifest rollback', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const releases = [
remoteReleaseForVersion('2.16.0', 'b'.repeat(64)),
remoteReleaseForVersion('2.17.0', 'c'.repeat(64)),
remoteReleaseForVersion('2.16.0', 'b'.repeat(64)),
];
let requests = 0;
const fetchImpl = async () => response(releases[requests++]);
const first = await checkForUpdate(options(testFixture, fetchImpl));
assert.equal(first.status, 'update_available');
assert.deepEqual(await acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: first.eventKey,
}), { status: 'acknowledged', eventKey: first.eventKey });
const second = await checkForUpdate(options(testFixture, fetchImpl, {
now: () => baseTime + (73 * 60 * 60 * 1_000),
}));
assert.equal(second.status, 'update_available');
assert.deepEqual(await acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: second.eventKey,
}), { status: 'acknowledged', eventKey: second.eventKey });
assert.deepEqual(await checkForUpdate(options(testFixture, fetchImpl, {
now: () => baseTime + (146 * 60 * 60 * 1_000),
})), { status: 'silent', reason: 'already-notified' });
assert.equal(requests, 3);
assert.deepEqual(
JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).notification.acknowledgedDigests,
[first.targetDigest, second.targetDigest],
);
});
test('a 64 KiB multi-offer cache only returns an event whose acknowledgement closure can commit', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const earlierDigest = `sha256:${'a'.repeat(64)}`;
const currentDigest = `sha256:${'b'.repeat(64)}`;
const replacementDigest = `sha256:${'c'.repeat(64)}`;
const cachedVersion = `3.16.${'9'.repeat(35)}`;
const state = cachedStateWithHistory({
offeredDigests: [earlierDigest, currentDigest],
acknowledgedDigests: Array.from({ length: 877 }, (_, index) => historyDigest(index)),
});
state.check.nextCheckAt = 'Mon, 31 Aug 2026 08:00:00 GMT';
state.candidate = {
version: cachedVersion,
targetDigest: currentDigest,
severity: 'normal',
releaseNotes: `https://github.com/tt-a1i/archify/releases/tag/v${cachedVersion}`,
};
assert.equal(Buffer.byteLength(compactStateSource(state)), maxCacheStateBytes);
writeCompactCommittedState(testFixture, state);
let requests = 0;
const offered = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteReleaseForVersion('2.16.0', 'c'.repeat(64)));
}));
assert.equal(offered.status, 'update_available');
const acknowledgement = await acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: offered.eventKey,
});
assert.deepEqual(acknowledgement, {
status: 'acknowledged',
eventKey: offered.eventKey,
});
assert.equal(requests, 1, 'an unrecoverable cached offer must be rebuilt before exposure');
assert.equal(offered.targetDigest, replacementDigest);
});
test('a recoverable multi-offer boundary acknowledges every event without pruning history', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const earlierDigest = `sha256:${'a'.repeat(64)}`;
const currentDigest = `sha256:${'b'.repeat(64)}`;
const cachedVersion = `3.16.${'9'.repeat(35)}`;
const exactHistory = Array.from({ length: 877 }, (_, index) => historyDigest(index));
const state = cachedStateWithHistory({
offeredDigests: [earlierDigest, currentDigest],
acknowledgedDigests: exactHistory,
});
state.check.nextCheckAt = 'Mon, 31 Aug 2026 8:00:00 GMT';
state.candidate = {
version: cachedVersion,
targetDigest: currentDigest,
severity: 'normal',
releaseNotes: `https://github.com/tt-a1i/archify/releases/tag/v${cachedVersion}`,
};
assert.equal(Buffer.byteLength(compactStateSource(state)), maxCacheStateBytes - 1);
writeCompactCommittedState(testFixture, state);
const current = await checkForUpdate(options(testFixture, async () => {
throw new Error('a fresh recoverable cache must not use the network');
}));
assert.equal(current.targetDigest, currentDigest);
assert.deepEqual(await acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: current.eventKey,
}), { status: 'acknowledged', eventKey: current.eventKey });
const earlierEventKey = `archify@${earlierDigest}`;
assert.deepEqual(await acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: earlierEventKey,
}), { status: 'acknowledged', eventKey: earlierEventKey });
const persisted = JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8'));
assert.deepEqual(persisted.notification, {
offeredDigests: [],
acknowledgedDigests: [...exactHistory, currentDigest, earlierDigest],
});
assert.equal(fs.statSync(statePath(testFixture)).size, maxCacheStateBytes);
});
test('a near-capacity null schedule does not retry the network on every activation', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const state = cachedStateWithHistory({
acknowledgedDigests: Array.from({ length: 883 }, (_, index) => historyDigest(index)),
});
state.check.nextCheckAt = null;
assert.equal(Buffer.byteLength(compactStateSource(state)), 65_524);
writeCompactCommittedState(testFixture, state);
let requests = 0;
const fetchImpl = async () => {
requests += 1;
return response(remoteRelease());
};
const first = await checkForUpdate(options(testFixture, fetchImpl));
const second = await checkForUpdate(options(testFixture, fetchImpl));
assert.equal(first.status, 'update_available');
assert.deepEqual(second, first);
assert.equal(requests, 1, 'the first check must commit a bounded retry or reusable candidate');
});
test('a saturated exact acknowledgement history never returns an unacknowledgeable offer', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const targetDigest = `sha256:${'f'.repeat(64)}`;
const state = cachedStateWithHistory();
let projected = candidateStateForDigest(state, targetDigest);
let index = 0;
while (Buffer.byteLength(compactStateSource(projected)) <= maxCacheStateBytes) {
state.notification.acknowledgedDigests.push(historyDigest(index));
index += 1;
projected = candidateStateForDigest(state, targetDigest);
}
assert.ok(Buffer.byteLength(compactStateSource(state)) <= maxCacheStateBytes);
assert.ok(Buffer.byteLength(compactStateSource(projected)) > maxCacheStateBytes);
const exactHistory = [...state.notification.acknowledgedDigests];
writeCompactCommittedState(testFixture, state);
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteReleaseForVersion('2.16.0', 'f'.repeat(64)));
}));
const acknowledgement = result.status === 'update_available'
? await acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: result.eventKey,
})
: null;
assert.deepEqual({ result, acknowledgement }, {
result: { status: 'silent', reason: 'cache-unavailable' },
acknowledgement: null,
});
assert.equal(requests, 1);
assert.ok(committedStateFiles(testFixture).every(
(target) => fs.statSync(target).size <= maxCacheStateBytes,
));
assert.deepEqual(
JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).notification.acknowledgedDigests,
exactHistory,
);
assert.deepEqual(await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteReleaseForVersion('2.16.0', 'f'.repeat(64)));
})), { status: 'silent', reason: 'cache-valid' });
assert.equal(requests, 1, 'capacity rejection should commit a bounded retry delay');
});
test('capacity backoff withdraws a stale candidate while preserving its late acknowledgement', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const staleDigest = `sha256:${'b'.repeat(64)}`;
const replacementDigest = `sha256:${'c'.repeat(64)}`;
const state = cachedStateWithHistory({ offeredDigests: [staleDigest] });
state.candidate = {
version: '2.16.0',
targetDigest: staleDigest,
severity: 'normal',
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0',
};
let replacement = candidateStateForDigest(state, replacementDigest);
let index = 0;
while (Buffer.byteLength(compactStateSource(replacement)) <= maxCacheStateBytes) {
state.notification.acknowledgedDigests.push(historyDigest(index));
index += 1;
replacement = candidateStateForDigest(state, replacementDigest);
}
assert.ok(Buffer.byteLength(compactStateSource(state)) <= maxCacheStateBytes);
assert.ok(Buffer.byteLength(compactStateSource(replacement)) > maxCacheStateBytes);
const exactHistory = [...state.notification.acknowledgedDigests];
writeCompactCommittedState(testFixture, state);
let requests = 0;
const fetchImpl = async () => {
requests += 1;
return response(remoteReleaseForVersion('2.16.0', 'c'.repeat(64)));
};
const refresh = await checkForUpdate(options(testFixture, fetchImpl));
const cached = await checkForUpdate(options(testFixture, fetchImpl));
const staleEventKey = `archify@${staleDigest}`;
const lateAcknowledgement = await acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: staleEventKey,
});
assert.deepEqual({ refresh, cached, lateAcknowledgement }, {
refresh: { status: 'silent', reason: 'cache-unavailable' },
cached: { status: 'silent', reason: 'cache-valid' },
lateAcknowledgement: { status: 'acknowledged', eventKey: staleEventKey },
});
assert.equal(requests, 1);
assert.ok(committedStateFiles(testFixture).every(
(target) => fs.statSync(target).size <= maxCacheStateBytes,
));
const persisted = JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8'));
assert.equal(Object.hasOwn(persisted, 'candidate'), false);
assert.deepEqual(persisted.notification, {
offeredDigests: [],
acknowledgedDigests: [...exactHistory, staleDigest],
});
});
test('a 64 KiB state can be acknowledged but a 64 KiB plus one state is ignored', async () => {
const targetDigest = `sha256:${'e'.repeat(64)}`;
const exactState = cachedStateWithHistory({ offeredDigests: [targetDigest] });
exactState.candidate = {
version: '2.16.0',
targetDigest,
severity: 'normal',
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0',
};
const initialBytes = Buffer.byteLength(compactStateSource(exactState));
exactState.check.nextCheckAt += 'x'.repeat(maxCacheStateBytes - initialBytes);
assert.equal(Buffer.byteLength(compactStateSource(exactState)), maxCacheStateBytes);
const exactFixture = fixture();
try {
writeCompactCommittedState(exactFixture, exactState);
const eventKey = `archify@${targetDigest}`;
assert.deepEqual(await acknowledgeUpdate({
releasePath: exactFixture.releasePath,
cacheDirectory: exactFixture.cacheDirectory,
eventKey,
}), { status: 'acknowledged', eventKey });
assert.equal(fs.statSync(statePath(exactFixture)).size, maxCacheStateBytes);
assert.deepEqual(
JSON.parse(fs.readFileSync(statePath(exactFixture), 'utf8')).notification,
{ offeredDigests: [], acknowledgedDigests: [targetDigest] },
);
} finally {
fs.rmSync(exactFixture.root, { recursive: true, force: true });
}
const oversizedFixture = fixture();
try {
const target = writeCompactCommittedState(oversizedFixture, exactState);
fs.appendFileSync(target, ' ');
assert.equal(fs.statSync(target).size, maxCacheStateBytes + 1);
assert.deepEqual(await acknowledgeUpdate({
releasePath: oversizedFixture.releasePath,
cacheDirectory: oversizedFixture.cacheDirectory,
eventKey: `archify@${targetDigest}`,
}), { status: 'silent', reason: 'invalid-acknowledgement' });
} finally {
fs.rmSync(oversizedFixture.root, { recursive: true, force: true });
}
});
test('a boundary offer remains acknowledgeable without pruning exact history', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const targetDigest = `sha256:${'d'.repeat(64)}`;
const state = cachedStateWithHistory();
let index = 0;
while (true) {
state.notification.acknowledgedDigests.push(historyDigest(index));
if (Buffer.byteLength(compactStateSource(
candidateStateForDigest(state, targetDigest),
)) > maxCacheStateBytes) {
state.notification.acknowledgedDigests.pop();
break;
}
index += 1;
}
const projected = candidateStateForDigest(state, targetDigest);
assert.ok(Buffer.byteLength(compactStateSource(projected)) <= maxCacheStateBytes);
const oneMoreAcknowledgement = cachedStateWithHistory({
acknowledgedDigests: [...state.notification.acknowledgedDigests, historyDigest(index)],
});
assert.ok(Buffer.byteLength(compactStateSource(
candidateStateForDigest(oneMoreAcknowledgement, targetDigest),
)) > maxCacheStateBytes);
const exactHistory = [...state.notification.acknowledgedDigests];
writeCompactCommittedState(testFixture, state);
const fetchImpl = async () => response(remoteReleaseForVersion('2.16.0', 'd'.repeat(64)));
const offered = await checkForUpdate(options(testFixture, fetchImpl));
assert.equal(offered.status, 'update_available');
assert.deepEqual(await acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: offered.eventKey,
}), { status: 'acknowledged', eventKey: offered.eventKey });
assert.ok(committedStateFiles(testFixture).every(
(target) => fs.statSync(target).size <= maxCacheStateBytes,
));
assert.deepEqual(
JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).notification,
{
offeredDigests: [],
acknowledgedDigests: [...exactHistory, targetDigest],
},
);
assert.deepEqual(await checkForUpdate(options(testFixture, fetchImpl)), {
status: 'silent',
reason: 'already-notified',
});
});
test('opaque response validators are neither persisted nor replayed after the check TTL', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
let requests = 0;
const fetchImpl = async (_url, init) => {
requests += 1;
assert.equal(init.headers['if-none-match'], undefined);
return response(remoteRelease(), { etag: `"per-client-${requests}"` });
};
const first = await checkForUpdate(options(testFixture, fetchImpl));
assert.equal(first.status, 'update_available');
const second = await checkForUpdate(options(testFixture, fetchImpl, {
now: () => baseTime + (73 * 60 * 60 * 1_000),
}));
assert.equal(second.status, 'update_available');
assert.equal(second.eventKey, first.eventKey);
assert.equal(requests, 2);
assert.equal(Object.hasOwn(JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).check, 'etag'), false);
});
test('an HTTP 304 is always a failed unconditional refresh', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
let requests = 0;
const fetchImpl = async () => {
requests += 1;
if (requests === 1) {
return new Response(JSON.stringify(remoteRelease()), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
return new Response(null, { status: 304 });
};
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
const refresh = await checkForUpdate(options(testFixture, fetchImpl, {
now: () => baseTime + (73 * 60 * 60 * 1_000),
}));
assert.deepEqual(refresh, { status: 'silent', reason: 'check-failed' });
assert.equal(requests, 2);
});
test('a failed refresh preserves the last-good unacknowledged candidate', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
let requests = 0;
const fetchImpl = async () => {
requests += 1;
if (requests === 1) return response(remoteRelease());
throw new Error('offline');
};
const first = await checkForUpdate(options(testFixture, fetchImpl));
assert.equal(first.status, 'update_available');
const failed = await checkForUpdate(options(testFixture, fetchImpl, {
now: () => baseTime + (73 * 60 * 60 * 1_000),
}));
assert.deepEqual(failed, { status: 'silent', reason: 'check-failed' });
const cached = await checkForUpdate(options(testFixture, fetchImpl, {
now: () => baseTime + (74 * 60 * 60 * 1_000),
}));
assert.equal(cached.status, 'update_available');
assert.equal(cached.eventKey, first.eventKey);
assert.equal(requests, 2);
});
test('failure backoff saturates safely instead of overflowing the cache counter', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
writeJson(statePath(testFixture), {
schemaVersion: 1,
skillId: 'archify',
installedVersion: '2.15.0',
check: {
nextCheckAt: new Date(baseTime - 1_000).toISOString(),
consecutiveFailures: Number.MAX_SAFE_INTEGER,
},
notification: {
offeredDigests: [],
acknowledgedDigests: [],
},
});
let requests = 0;
const fetchImpl = async () => {
requests += 1;
throw new Error('offline');
};
assert.deepEqual(await checkForUpdate(options(testFixture, fetchImpl)), {
status: 'silent',
reason: 'check-failed',
});
assert.deepEqual(await checkForUpdate(options(testFixture, fetchImpl, {
now: () => baseTime + (60 * 60 * 1_000),
})), { status: 'silent', reason: 'cache-valid' });
assert.equal(requests, 1);
assert.equal(JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).check.consecutiveFailures, 2);
});
test('a committed cache FIFO is ignored without blocking the update check', (t) => {
if (process.platform === 'win32') {
t.skip('FIFO files are unavailable on Windows');
return;
}
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const cacheStatePath = statePath(testFixture);
fs.mkdirSync(path.dirname(cacheStatePath), { recursive: true });
if (!createFifo(cacheStatePath)) {
t.skip('mkfifo is unavailable');
return;
}
assertUnsafeCacheStateIsIgnored(testFixture);
});
test('a committed cache state symlink is ignored without following its target', (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const cacheStatePath = statePath(testFixture);
const linkedTarget = path.join(testFixture.root, 'linked-state-target');
fs.mkdirSync(path.dirname(cacheStatePath), { recursive: true });
const targetIsFifo = process.platform !== 'win32' && createFifo(linkedTarget);
if (!targetIsFifo) writeJson(linkedTarget, cachedUpdateState());
fs.symlinkSync(linkedTarget, cacheStatePath, process.platform === 'win32' ? 'file' : undefined);
assertUnsafeCacheStateIsIgnored(testFixture);
});
test('an oversized committed cache state is ignored without an unbounded read', (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const oversizedState = `${JSON.stringify(cachedUpdateState())}${' '.repeat(256 * 1_024)}`;
const cacheStatePath = statePath(testFixture);
fs.mkdirSync(path.dirname(cacheStatePath), { recursive: true });
fs.writeFileSync(cacheStatePath, oversizedState);
assertUnsafeCacheStateIsIgnored(testFixture);
});
test('a cache ancestor replaced before a cached read cannot inject a reminder', async (t) => {
const testFixture = fixture();
const originalReaddir = fsPromises.readdir;
t.after(() => {
fsPromises.readdir = originalReaddir;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
writeCompactCommittedState(testFixture, cachedStateWithHistory());
const replacementRoot = path.join(testFixture.root, 'replacement-cache');
writeJson(
path.join(replacementRoot, path.basename(stateDirectory(testFixture)),
path.basename(operationPath(testFixture, 'committed', 1n)), 'state.json'),
cachedUpdateState(),
);
const detachedCache = path.join(testFixture.root, 'detached-cache');
const probe = path.join(testFixture.root, 'symlink-probe');
try {
fs.symlinkSync(
replacementRoot,
probe,
process.platform === 'win32' ? 'junction' : 'dir',
);
fs.unlinkSync(probe);
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`directory symlinks are unavailable: ${error.code}`);
return;
}
throw error;
}
let replaced = false;
fsPromises.readdir = async (target, ...args) => {
if (!replaced && path.resolve(target) === path.resolve(stateDirectory(testFixture))) {
replaced = true;
fs.renameSync(testFixture.cacheDirectory, detachedCache);
fs.symlinkSync(
replacementRoot,
testFixture.cacheDirectory,
process.platform === 'win32' ? 'junction' : 'dir',
);
}
return originalReaddir(target, ...args);
};
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.equal(replaced, true);
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
assert.equal(requests, 0);
});
test('a committed directory replaced by a symlink cannot inject a reminder', async (t) => {
const testFixture = fixture();
const originalReaddir = fsPromises.readdir;
t.after(() => {
fsPromises.readdir = originalReaddir;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
writeCompactCommittedState(testFixture, cachedStateWithHistory());
const committed = operationPath(testFixture, 'committed', 1n);
const replacement = path.join(testFixture.root, 'replacement-committed');
writeJson(path.join(replacement, 'state.json'), cachedUpdateState());
const probe = path.join(testFixture.root, 'nested-symlink-probe');
try {
fs.symlinkSync(
replacement,
probe,
process.platform === 'win32' ? 'junction' : 'dir',
);
fs.unlinkSync(probe);
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`directory symlinks are unavailable: ${error.code}`);
return;
}
throw error;
}
const detached = path.join(stateDirectory(testFixture), 'detached-committed');
let replaced = false;
fsPromises.readdir = async (target, ...args) => {
const entries = await originalReaddir(target, ...args);
if (!replaced && path.resolve(target) === path.resolve(stateDirectory(testFixture))) {
replaced = true;
fs.renameSync(committed, detached);
fs.symlinkSync(
replacement,
committed,
process.platform === 'win32' ? 'junction' : 'dir',
);
}
return entries;
};
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.equal(replaced, true);
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
assert.equal(requests, 0);
});
test('a pending directory replaced by a symlink cannot be ignored as inactive', async (t) => {
const testFixture = fixture();
const originalReaddir = fsPromises.readdir;
t.after(() => {
fsPromises.readdir = originalReaddir;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
const pending = writePendingOperation(testFixture);
const replacement = path.join(testFixture.root, 'replacement-pending');
writePendingOperation({
...testFixture,
cacheDirectory: path.join(testFixture.root, 'replacement-cache'),
}, { modifiedAt: new Date(Date.now() - 60_000) });
const replacementPending = operationPath({
...testFixture,
cacheDirectory: path.join(testFixture.root, 'replacement-cache'),
}, 'pending', 1n);
fs.renameSync(replacementPending, replacement);
const probe = path.join(testFixture.root, 'pending-symlink-probe');
try {
fs.symlinkSync(
replacement,
probe,
process.platform === 'win32' ? 'junction' : 'dir',
);
fs.unlinkSync(probe);
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`directory symlinks are unavailable: ${error.code}`);
return;
}
throw error;
}
const detached = path.join(stateDirectory(testFixture), 'detached-pending');
let replaced = false;
fsPromises.readdir = async (target, ...args) => {
const entries = await originalReaddir(target, ...args);
if (!replaced && path.resolve(target) === path.resolve(stateDirectory(testFixture))) {
replaced = true;
fs.renameSync(pending, detached);
fs.symlinkSync(
replacement,
pending,
process.platform === 'win32' ? 'junction' : 'dir',
);
}
return entries;
};
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.equal(replaced, true);
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
assert.equal(requests, 0);
});
test('zero-inode cache metadata uses timestamp fallback to reject a replaced ancestor', async (t) => {
const testFixture = fixture();
const originalLstat = fsPromises.lstat;
const originalReaddir = fsPromises.readdir;
t.after(() => {
fsPromises.lstat = originalLstat;
fsPromises.readdir = originalReaddir;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
writeCompactCommittedState(testFixture, cachedStateWithHistory());
const replacementRoot = path.join(testFixture.root, 'replacement-cache');
writeJson(
path.join(replacementRoot, path.basename(stateDirectory(testFixture)),
path.basename(operationPath(testFixture, 'committed', 1n)), 'state.json'),
cachedUpdateState(),
);
const detachedCache = path.join(testFixture.root, 'detached-cache');
fsPromises.lstat = async (target, ...args) => {
const metadata = await originalLstat(target, ...args);
const resolved = path.resolve(target);
const isCachePath = resolved === path.resolve(testFixture.cacheDirectory)
|| resolved.startsWith(`${path.resolve(testFixture.cacheDirectory)}${path.sep}`);
if (args[0]?.bigint && isCachePath && metadata.isDirectory()) {
return metadataWithOverrides(metadata, {
ino: 0n,
birthtimeNs: replaced ? 2_000n : 1_000n,
});
}
return metadata;
};
let replaced = false;
fsPromises.readdir = async (target, ...args) => {
if (!replaced && path.resolve(target) === path.resolve(stateDirectory(testFixture))) {
replaced = true;
fs.renameSync(testFixture.cacheDirectory, detachedCache);
fs.renameSync(replacementRoot, testFixture.cacheDirectory);
}
return originalReaddir(target, ...args);
};
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.equal(replaced, true);
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
assert.equal(requests, 0);
});
test('zero-inode cache metadata still supports a normal check and acknowledgement', async (t) => {
const testFixture = fixture();
const originalLstat = fsPromises.lstat;
const originalOpen = fsPromises.open;
t.after(() => {
fsPromises.lstat = originalLstat;
fsPromises.open = originalOpen;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
const cacheRoot = path.resolve(testFixture.cacheDirectory);
const isCachePath = (target) => {
const resolved = path.resolve(target);
return resolved === cacheRoot || resolved.startsWith(`${cacheRoot}${path.sep}`);
};
fsPromises.lstat = async (target, ...args) => {
const metadata = await originalLstat(target, ...args);
if (args[0]?.bigint && isCachePath(target)) {
return metadataWithOverrides(metadata, { ino: 0n, birthtimeNs: 1_000n });
}
return metadata;
};
fsPromises.open = async (target, ...args) => {
const handle = await originalOpen(target, ...args);
if (!isCachePath(target)) return handle;
return new Proxy(handle, {
get(fileHandle, property) {
if (property === 'stat') {
return async (...statArguments) => {
const metadata = await fileHandle.stat(...statArguments);
return statArguments[0]?.bigint
? metadataWithOverrides(metadata, { ino: 0n, birthtimeNs: 1_000n })
: metadataWithOverrides(metadata, { ino: 0 });
};
}
const value = Reflect.get(fileHandle, property, fileHandle);
return typeof value === 'function' ? value.bind(fileHandle) : value;
},
});
};
let requests = 0;
const offered = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
const acknowledged = await acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: offered.eventKey,
});
assert.equal(offered.status, 'update_available');
assert.deepEqual(acknowledged, { status: 'acknowledged', eventKey: offered.eventKey });
assert.equal(requests, 1);
});
test('zero-inode cache metadata without birthtime fails closed', async (t) => {
const testFixture = fixture();
const originalLstat = fsPromises.lstat;
t.after(() => {
fsPromises.lstat = originalLstat;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
const cacheRoot = path.resolve(testFixture.cacheDirectory);
fsPromises.lstat = async (target, ...args) => {
const metadata = await originalLstat(target, ...args);
const resolved = path.resolve(target);
if (args[0]?.bigint
&& (resolved === cacheRoot || resolved.startsWith(`${cacheRoot}${path.sep}`))) {
return metadataWithOverrides(metadata, { ino: 0n, birthtimeNs: 0n });
}
return metadata;
};
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
assert.equal(requests, 0);
});
test('a state write replaced before post-verification is never reported as committed', async (t) => {
const testFixture = fixture();
const originalOpen = fsPromises.open;
const originalWriteFile = fsPromises.writeFile;
t.after(() => {
fsPromises.open = originalOpen;
fsPromises.writeFile = originalWriteFile;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
let replaced = false;
const replaceStateFile = (target) => {
if (replaced) return;
replaced = true;
fs.renameSync(target, `${target}.written-by-checker`);
fs.writeFileSync(target, '{}\n', { flag: 'wx', mode: 0o600 });
};
fsPromises.writeFile = async (target, ...args) => {
const result = await originalWriteFile(target, ...args);
if (path.basename(target).startsWith('.state.')) replaceStateFile(target);
return result;
};
fsPromises.open = async (target, ...args) => {
const handle = await originalOpen(target, ...args);
if (!path.basename(target).startsWith('.state.')) return handle;
return {
stat: handle.stat.bind(handle),
writeFile: handle.writeFile.bind(handle),
close: async () => {
const result = await handle.close();
replaceStateFile(target);
return result;
},
};
};
const result = await checkForUpdate(options(testFixture, async () => response(remoteRelease())));
assert.equal(replaced, true);
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
for (const committed of committedStateFiles(testFixture)) {
assert.notDeepEqual(JSON.parse(fs.readFileSync(committed, 'utf8')), {});
}
});
test('a pending directory replaced by a symlink cannot be reported as committed', async (t) => {
const testFixture = fixture();
const originalRename = fsPromises.rename;
t.after(() => {
fsPromises.rename = originalRename;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
const probe = path.join(testFixture.root, 'symlink-probe');
try {
fs.mkdirSync(probe);
const probeLink = `${probe}-link`;
fs.symlinkSync(probe, probeLink, process.platform === 'win32' ? 'junction' : 'dir');
fs.unlinkSync(probeLink);
fs.rmdirSync(probe);
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`directory symlinks are unavailable: ${error.code}`);
return;
}
throw error;
}
const detachedPending = path.join(testFixture.root, 'detached-pending');
let pending = null;
let replaced = false;
fsPromises.rename = async (source, destination, ...args) => {
if (!pending
&& path.basename(source).startsWith('.state.')
&& path.basename(destination) === 'state.json') {
const result = await originalRename(source, destination, ...args);
pending = path.dirname(destination);
return result;
}
if (!replaced
&& pending
&& path.resolve(source) === path.resolve(pending)
&& path.basename(destination).startsWith('committed-')) {
replaced = true;
fs.renameSync(pending, detachedPending);
fs.symlinkSync(
detachedPending,
pending,
process.platform === 'win32' ? 'junction' : 'dir',
);
}
return originalRename(source, destination, ...args);
};
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.equal(replaced, true);
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
assert.equal(requests, 1);
assert.equal(fs.existsSync(path.join(detachedPending, 'state.json')), true);
});
test('a cache trust failure while cancelling cannot return a cached reminder', async (t) => {
const testFixture = fixture();
const originalRename = fsPromises.rename;
t.after(() => {
fsPromises.rename = originalRename;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
const stale = cachedUpdateState();
stale.check.nextCheckAt = new Date(baseTime - 1_000).toISOString();
const committed = writeCompactCommittedState(testFixture, stale);
const detachedCache = path.join(testFixture.root, 'detached-cache');
let refreshed = false;
let replaced = false;
fsPromises.rename = async (source, destination, ...args) => {
const sourceName = path.basename(source);
const destinationName = path.basename(destination);
if (!refreshed && sourceName.startsWith('claim-') && destinationName === 'active-claim') {
const result = await originalRename(source, destination, ...args);
const fresh = cachedUpdateState();
writeJson(committed, fresh);
refreshed = true;
return result;
}
if (!replaced && sourceName.startsWith('pending-') && destinationName.startsWith('cancelled-')) {
replaced = true;
fs.renameSync(testFixture.cacheDirectory, detachedCache);
fs.symlinkSync(
detachedCache,
testFixture.cacheDirectory,
process.platform === 'win32' ? 'junction' : 'dir',
);
}
return originalRename(source, destination, ...args);
};
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.equal(refreshed, true);
assert.equal(replaced, true);
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
assert.equal(requests, 0);
});
test('prepared claim cleanup never recursively deletes through a replaced cache ancestor', async (t) => {
const testFixture = fixture();
const originalOpen = fsPromises.open;
const originalRm = fsPromises.rm;
const originalWriteFile = fsPromises.writeFile;
t.after(() => {
fsPromises.open = originalOpen;
fsPromises.rm = originalRm;
fsPromises.writeFile = originalWriteFile;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
const activeClaim = path.join(stateDirectory(testFixture), 'active-claim');
let injectedActiveClaim = false;
const injectActiveClaim = () => {
if (injectedActiveClaim) return;
injectedActiveClaim = true;
fs.mkdirSync(activeClaim);
};
fsPromises.writeFile = async (target, ...args) => {
const result = await originalWriteFile(target, ...args);
if (path.basename(path.dirname(target)).startsWith('claim-')
&& path.basename(target) === 'owner.json') injectActiveClaim();
return result;
};
fsPromises.open = async (target, ...args) => {
const handle = await originalOpen(target, ...args);
if (path.basename(path.dirname(target)).startsWith('claim-')
&& path.basename(target) === 'owner.json') {
return {
stat: handle.stat.bind(handle),
writeFile: handle.writeFile.bind(handle),
close: async () => {
const result = await handle.close();
injectActiveClaim();
return result;
},
};
}
return handle;
};
const victimRoot = path.join(testFixture.root, 'victim-cache');
const victimClaim = path.join(
victimRoot,
path.basename(stateDirectory(testFixture)),
path.basename(operationPath(testFixture, 'claim', 1n)),
);
const victimFile = path.join(victimClaim, 'important.txt');
fs.mkdirSync(victimClaim, { recursive: true });
fs.writeFileSync(victimFile, 'must survive\n');
const detachedCache = path.join(testFixture.root, 'detached-cache');
let interceptedRemoval = false;
fsPromises.rm = async (target, ...args) => {
if (!interceptedRemoval && path.basename(target).startsWith('claim-')) {
interceptedRemoval = true;
fs.renameSync(testFixture.cacheDirectory, detachedCache);
fs.symlinkSync(
victimRoot,
testFixture.cacheDirectory,
process.platform === 'win32' ? 'junction' : 'dir',
);
}
return originalRm(target, ...args);
};
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.equal(injectedActiveClaim, true);
assert.equal(requests, 0);
assert.equal(result.status, 'silent');
assert.equal(interceptedRemoval, false);
assert.equal(fs.readFileSync(victimFile, 'utf8'), 'must survive\n');
const coordinationEntries = fs.readdirSync(stateDirectory(testFixture));
assert.deepEqual(
coordinationEntries.filter((name) => name.startsWith('claim-')),
[],
);
const discardedClaims = coordinationEntries
.filter((name) => name.startsWith('discarded-claim-'));
assert.equal(discardedClaims.length, 1);
const discardedOwnerPath = path.join(
stateDirectory(testFixture),
discardedClaims[0],
'owner.json',
);
const discardedOwner = JSON.parse(fs.readFileSync(discardedOwnerPath, 'utf8'));
assert.deepEqual(Object.keys(discardedOwner).sort(), ['generation', 'pid', 'token']);
assert.equal(discardedOwner.generation, '1');
assert.match(discardedOwner.token, /^[a-f0-9]{32}$/);
assert.equal(discardedClaims[0].endsWith(`-${discardedOwner.token}`), true);
});
test('a symlink cache root cannot write into its target', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const protectedTarget = path.join(testFixture.root, 'simulated-project-config');
fs.mkdirSync(protectedTarget);
fs.writeFileSync(path.join(protectedTarget, 'settings.json'), '{"protected":true}\n');
try {
fs.symlinkSync(
protectedTarget,
testFixture.cacheDirectory,
process.platform === 'win32' ? 'junction' : 'dir',
);
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`directory symlinks are unavailable: ${error.code}`);
return;
}
throw error;
}
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
assert.equal(requests, 0);
assert.deepEqual(fs.readdirSync(protectedTarget), ['settings.json']);
});
test('a symlink cache ancestor cannot redirect cache writes', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const protectedTarget = path.join(testFixture.root, 'simulated-project-config');
const linkedAncestor = path.join(testFixture.root, 'cache-parent');
const cacheDirectory = path.join(linkedAncestor, 'nested', 'cache');
fs.mkdirSync(protectedTarget);
fs.writeFileSync(path.join(protectedTarget, 'settings.json'), '{"protected":true}\n');
try {
fs.symlinkSync(
protectedTarget,
linkedAncestor,
process.platform === 'win32' ? 'junction' : 'dir',
);
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`directory symlinks are unavailable: ${error.code}`);
return;
}
throw error;
}
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}, { cacheDirectory }));
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
assert.equal(requests, 0);
assert.deepEqual(fs.readdirSync(protectedTarget), ['settings.json']);
});
test('a cache ancestor replaced during preparation cannot redirect later writes', async (t) => {
const testFixture = fixture();
const originalLstat = fsPromises.lstat;
t.after(() => {
fsPromises.lstat = originalLstat;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
const protectedTarget = path.join(testFixture.root, 'simulated-project-config');
const protectedVersion = path.join(protectedTarget, path.basename(stateDirectory(testFixture)));
const detachedCache = path.join(testFixture.root, 'detached-cache');
const probe = path.join(testFixture.root, 'symlink-probe');
fs.mkdirSync(testFixture.cacheDirectory);
fs.mkdirSync(protectedVersion, { recursive: true });
fs.writeFileSync(path.join(protectedVersion, 'settings.json'), '{"protected":true}\n');
try {
fs.symlinkSync(
protectedTarget,
probe,
process.platform === 'win32' ? 'junction' : 'dir',
);
fs.unlinkSync(probe);
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`directory symlinks are unavailable: ${error.code}`);
return;
}
throw error;
}
let replaced = false;
const versionDirectoryName = path.basename(stateDirectory(testFixture));
fsPromises.lstat = async (target, ...args) => {
if (!replaced && path.basename(target) === versionDirectoryName) {
replaced = true;
fs.renameSync(testFixture.cacheDirectory, detachedCache);
fs.symlinkSync(
protectedTarget,
testFixture.cacheDirectory,
process.platform === 'win32' ? 'junction' : 'dir',
);
}
return originalLstat(target, ...args);
};
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.equal(replaced, true);
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
assert.equal(requests, 0);
assert.deepEqual(fs.readdirSync(protectedVersion), ['settings.json']);
});
test('a cache ancestor replaced before reservation creation fails closed', async (t) => {
const testFixture = fixture();
const originalMkdir = fsPromises.mkdir;
t.after(() => {
fsPromises.mkdir = originalMkdir;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
const replacementRoot = path.join(testFixture.root, 'replacement-cache');
const replacementVersion = path.join(
replacementRoot,
path.basename(stateDirectory(testFixture)),
);
const detachedCache = path.join(testFixture.root, 'detached-cache');
const probe = path.join(testFixture.root, 'symlink-probe');
fs.mkdirSync(replacementVersion, { recursive: true });
fs.writeFileSync(path.join(replacementVersion, 'settings.json'), '{"protected":true}\n');
try {
fs.symlinkSync(
replacementRoot,
probe,
process.platform === 'win32' ? 'junction' : 'dir',
);
fs.unlinkSync(probe);
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`directory symlinks are unavailable: ${error.code}`);
return;
}
throw error;
}
let replaced = false;
fsPromises.mkdir = async (target, ...args) => {
if (!replaced && path.basename(target).startsWith('reserved-')) {
replaced = true;
fs.renameSync(testFixture.cacheDirectory, detachedCache);
fs.symlinkSync(
replacementRoot,
testFixture.cacheDirectory,
process.platform === 'win32' ? 'junction' : 'dir',
);
}
return originalMkdir(target, ...args);
};
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.equal(replaced, true);
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
assert.equal(requests, 0);
const replacementEntries = fs.readdirSync(replacementVersion).sort();
const reservations = replacementEntries.filter((entry) => entry.startsWith('reserved-'));
assert.equal(reservations.length, 1);
assert.deepEqual(
replacementEntries.filter((entry) => !entry.startsWith('reserved-')),
['settings.json'],
);
assert.deepEqual(fs.readdirSync(path.join(replacementVersion, reservations[0])), []);
});
test('a cache ancestor replacement restored after reservation creation fails closed', async (t) => {
const testFixture = fixture();
const originalMkdir = fsPromises.mkdir;
t.after(() => {
fsPromises.mkdir = originalMkdir;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
const replacementRoot = path.join(testFixture.root, 'replacement-cache');
const replacementVersion = path.join(
replacementRoot,
path.basename(stateDirectory(testFixture)),
);
const detachedCache = path.join(testFixture.root, 'detached-cache');
const probe = path.join(testFixture.root, 'symlink-probe');
fs.mkdirSync(replacementVersion, { recursive: true });
fs.writeFileSync(path.join(replacementVersion, 'settings.json'), '{"protected":true}\n');
try {
fs.symlinkSync(
replacementRoot,
probe,
process.platform === 'win32' ? 'junction' : 'dir',
);
fs.unlinkSync(probe);
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`directory symlinks are unavailable: ${error.code}`);
return;
}
throw error;
}
let restored = false;
fsPromises.mkdir = async (target, ...args) => {
if (!restored && path.basename(target).startsWith('reserved-')) {
fs.renameSync(testFixture.cacheDirectory, detachedCache);
fs.symlinkSync(
replacementRoot,
testFixture.cacheDirectory,
process.platform === 'win32' ? 'junction' : 'dir',
);
try {
return await originalMkdir(target, ...args);
} finally {
fs.unlinkSync(testFixture.cacheDirectory);
fs.renameSync(detachedCache, testFixture.cacheDirectory);
restored = true;
}
}
return originalMkdir(target, ...args);
};
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.equal(restored, true);
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
assert.equal(requests, 0);
const replacementEntries = fs.readdirSync(replacementVersion).sort();
const reservations = replacementEntries.filter((entry) => entry.startsWith('reserved-'));
assert.equal(reservations.length, 1);
assert.deepEqual(
replacementEntries.filter((entry) => !entry.startsWith('reserved-')),
['settings.json'],
);
assert.deepEqual(fs.readdirSync(path.join(replacementVersion, reservations[0])), []);
});
test('a trusted cache prefix switched after validation cannot redirect later writes', async (t) => {
const testFixture = fixture();
const originalHomedir = os.homedir;
const originalLstat = fsPromises.lstat;
t.after(() => {
os.homedir = originalHomedir;
fsPromises.lstat = originalLstat;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
const trustedAlias = path.join(testFixture.root, 'trusted-home');
const canonicalHome = path.join(testFixture.root, 'canonical-home');
const protectedTarget = path.join(testFixture.root, 'simulated-project-config');
const cacheDirectory = path.join(trustedAlias, 'cache');
fs.mkdirSync(canonicalHome);
fs.mkdirSync(protectedTarget);
fs.writeFileSync(path.join(protectedTarget, 'settings.json'), '{"protected":true}\n');
try {
fs.symlinkSync(
canonicalHome,
trustedAlias,
process.platform === 'win32' ? 'junction' : 'dir',
);
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`directory symlinks are unavailable: ${error.code}`);
return;
}
throw error;
}
os.homedir = () => trustedAlias;
const canonicalVersionDirectory = path.join(
fs.realpathSync(canonicalHome),
'cache',
path.basename(stateDirectory(testFixture)),
);
let targetVisits = 0;
let switched = false;
fsPromises.lstat = async (target, ...args) => {
const metadata = await originalLstat(target, ...args);
if (!switched && path.resolve(target) === canonicalVersionDirectory) {
targetVisits += 1;
if (targetVisits === 2) {
switched = true;
fs.unlinkSync(trustedAlias);
fs.symlinkSync(
protectedTarget,
trustedAlias,
process.platform === 'win32' ? 'junction' : 'dir',
);
}
}
return metadata;
};
const result = await checkForUpdate(options(testFixture, async () => response(remoteRelease()), {
cacheDirectory,
}));
assert.equal(switched, true);
assert.equal(result.status, 'update_available');
assert.deepEqual(fs.readdirSync(protectedTarget), ['settings.json']);
assert.ok(fs.readdirSync(canonicalVersionDirectory).some(
(entry) => entry.startsWith('committed-'),
));
});
test('concurrent checks use one writer and leave a valid cache', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
let releaseFetch;
let markStarted;
const started = new Promise((resolve) => { markStarted = resolve; });
const fetchImpl = async () => {
markStarted();
return new Promise((resolve) => { releaseFetch = resolve; });
};
const firstCheck = checkForUpdate(options(testFixture, fetchImpl));
await started;
const overlapping = await checkForUpdate(options(testFixture, fetchImpl));
assert.deepEqual(overlapping, { status: 'silent', reason: 'check-in-progress' });
releaseFetch(response(remoteRelease()));
const first = await firstCheck;
assert.equal(first.status, 'update_available');
assert.doesNotThrow(() => JSON.parse(
fs.readFileSync(statePath(testFixture), 'utf8'),
));
});
test('an empty precheck snapshot cannot start a second concurrent network request', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
fs.mkdirSync(stateDirectory(testFixture), { recursive: true });
const pause = pauseReaddirSnapshot(stateDirectory(testFixture), 2);
t.after(() => pause.restore());
let releaseFetch;
let markFetchStarted;
let requests = 0;
const fetchStarted = new Promise((resolve) => { markFetchStarted = resolve; });
const fetchImpl = async () => {
requests += 1;
markFetchStarted();
return new Promise((resolve) => { releaseFetch = resolve; });
};
const delayedPrecheck = checkForUpdate(options(testFixture, fetchImpl));
await pause.reached;
const claimedCheck = checkForUpdate(options(testFixture, fetchImpl));
await fetchStarted;
pause.release();
assert.deepEqual(await delayedPrecheck, { status: 'silent', reason: 'check-in-progress' });
assert.equal(requests, 1);
releaseFetch(response(remoteRelease()));
assert.equal((await claimedCheck).status, 'update_available');
pause.restore();
assert.equal(requests, 1);
});
test('two promoters cannot replace and then steal a stale empty active claim', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const activeClaim = path.join(stateDirectory(testFixture), 'active-claim');
fs.mkdirSync(activeClaim, { recursive: true });
const staleTime = new Date(Date.now() - 60_000);
fs.utimesSync(activeClaim, staleTime, staleTime);
const pause = pauseWriteOnce('.retirement-guard');
t.after(() => pause.restore());
let releaseFetch;
let markFetchStarted;
let requests = 0;
const fetchStarted = new Promise((resolve) => { markFetchStarted = resolve; });
const fetchImpl = async () => {
requests += 1;
markFetchStarted();
return new Promise((resolve) => { releaseFetch = resolve; });
};
const delayedRetirement = checkForUpdate(options(testFixture, fetchImpl));
await pause.reached;
fs.utimesSync(
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
staleTime,
staleTime,
);
const claimedCheck = checkForUpdate(options(testFixture, fetchImpl));
await fetchStarted;
pause.release();
assert.deepEqual(await delayedRetirement, { status: 'silent', reason: 'check-in-progress' });
assert.equal(requests, 1);
releaseFetch(response(remoteRelease()));
assert.equal((await claimedCheck).status, 'update_available');
pause.restore();
assert.equal(requests, 1);
});
test('a delayed lower-generation promoter cannot retire a completed higher claim', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const pause = pauseRenameOnce(
operationPath(testFixture, 'claim', 1n),
path.join(stateDirectory(testFixture), 'active-claim'),
);
t.after(() => pause.restore());
let requests = 0;
const fetchImpl = async () => {
requests += 1;
return response(remoteRelease());
};
const delayedLower = checkForUpdate(options(testFixture, fetchImpl));
await pause.reached;
const staleTime = new Date(Date.now() - 60_000);
fs.utimesSync(
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
staleTime,
staleTime,
);
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
assert.equal(requests, 1);
pause.release();
assert.equal((await delayedLower).status, 'update_available');
pause.restore();
assert.equal(requests, 1);
assert.equal(
JSON.parse(fs.readFileSync(
path.join(stateDirectory(testFixture), 'active-claim', 'owner.json'),
'utf8',
)).generation,
'2',
);
});
test('an expired contender cannot fetch after its delayed claim promotion succeeds', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const pause = pauseRenameOnce(
operationPath(testFixture, 'claim', 1n),
path.join(stateDirectory(testFixture), 'active-claim'),
);
t.after(() => pause.restore());
let requests = 0;
const fetchImpl = async () => {
requests += 1;
return response(remoteRelease());
};
const delayedExpired = checkForUpdate(options(testFixture, fetchImpl));
await pause.reached;
const staleTime = new Date(Date.now() - 60_000);
fs.utimesSync(
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
staleTime,
staleTime,
);
pause.release();
assert.deepEqual(await delayedExpired, { status: 'silent', reason: 'check-in-progress' });
assert.equal(requests, 0);
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
pause.restore();
assert.equal(requests, 1);
assert.equal(
JSON.parse(fs.readFileSync(
path.join(stateDirectory(testFixture), 'active-claim', 'owner.json'),
'utf8',
)).generation,
'2',
);
});
test('a stale supersession snapshot cannot let a lower promoter retire a higher claim', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const generationOneClaim = path.basename(operationPath(testFixture, 'claim', 1n));
const pause = pauseReaddirSnapshotOnce(
stateDirectory(testFixture),
(entries) => entries.some((entry) => entry.name === generationOneClaim)
&& !entries.some((entry) => /-(?:0{19}2)$/.test(entry.name)),
);
t.after(() => pause.restore());
let requests = 0;
const fetchImpl = async () => {
requests += 1;
return response(remoteRelease());
};
const delayedLower = checkForUpdate(options(testFixture, fetchImpl));
await pause.reached;
const staleTime = new Date(Date.now() - 60_000);
fs.utimesSync(
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
staleTime,
staleTime,
);
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
assert.equal(requests, 1);
pause.release();
assert.equal((await delayedLower).status, 'update_available');
pause.restore();
assert.equal(requests, 1);
assert.equal(
JSON.parse(fs.readFileSync(
path.join(stateDirectory(testFixture), 'active-claim', 'owner.json'),
'utf8',
)).generation,
'2',
);
});
test('an owner fenced while active ownership is read cannot issue a second GET', {
timeout: parentCheckTimeoutMs,
}, async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const directory = stateDirectory(testFixture);
const originalOpen = fsPromises.open;
let releaseVerification;
let markVerification;
let intercepted = false;
const verificationReached = new Promise((resolve) => { markVerification = resolve; });
const verificationGate = new Promise((resolve) => { releaseVerification = resolve; });
fsPromises.open = async (target, ...args) => {
const handle = await originalOpen(target, ...args);
if (intercepted
|| path.resolve(target) !== path.join(directory, 'active-claim', 'owner.json')) {
return handle;
}
intercepted = true;
return new Proxy(handle, {
get(fileHandle, property) {
if (property === 'close') {
return async () => {
const result = await fileHandle.close();
markVerification();
await verificationGate;
return result;
};
}
const value = Reflect.get(fileHandle, property, fileHandle);
return typeof value === 'function' ? value.bind(fileHandle) : value;
},
});
};
t.after(() => {
releaseVerification();
fsPromises.open = originalOpen;
});
let requests = 0;
const checkOptions = options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
});
const suspendedOwner = checkForUpdate(checkOptions);
await verificationReached;
const staleTime = new Date(Date.now() - 60_000);
fs.utimesSync(
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
staleTime,
staleTime,
);
assert.equal((await checkForUpdate(checkOptions)).status, 'update_available');
assert.equal(requests, 1);
releaseVerification();
assert.deepEqual(await suspendedOwner, { status: 'silent', reason: 'cache-unavailable' });
assert.equal(requests, 1);
});
test('an expired active owner cannot start a network request', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const pendingOne = path.basename(operationPath(testFixture, 'pending', 1n));
const claimOne = path.basename(operationPath(testFixture, 'claim', 1n));
const pause = pauseReaddirSnapshotOnce(
stateDirectory(testFixture),
(entries) => entries.some((entry) => entry.name === 'active-claim')
&& entries.some((entry) => entry.name === pendingOne)
&& !entries.some((entry) => entry.name === claimOne),
);
t.after(() => pause.restore());
let requests = 0;
const check = checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
await pause.reached;
const staleTime = new Date(Date.now() - 60_000);
fs.utimesSync(
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
staleTime,
staleTime,
);
pause.release();
assert.deepEqual(await check, { status: 'silent', reason: 'check-in-progress' });
pause.restore();
assert.equal(requests, 0);
});
test('a fenced owner rechecks its active claim immediately before network access', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const pendingOne = path.basename(operationPath(testFixture, 'pending', 1n));
const claimOne = path.basename(operationPath(testFixture, 'claim', 1n));
const pause = pauseReaddirSnapshotOnce(
stateDirectory(testFixture),
(entries) => entries.some((entry) => entry.name === 'active-claim')
&& entries.some((entry) => entry.name === pendingOne)
&& !entries.some((entry) => entry.name === claimOne),
);
t.after(() => pause.restore());
let requests = 0;
let releaseFetch;
let markFetchStarted;
const fetchStarted = new Promise((resolve) => { markFetchStarted = resolve; });
const fetchImpl = async () => {
requests += 1;
markFetchStarted();
return new Promise((resolve) => { releaseFetch = resolve; });
};
const delayedOwner = checkForUpdate(options(testFixture, fetchImpl));
await pause.reached;
const staleTime = new Date(Date.now() - 60_000);
fs.utimesSync(
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
staleTime,
staleTime,
);
const successor = checkForUpdate(options(testFixture, fetchImpl));
await fetchStarted;
assert.equal(requests, 1);
pause.release();
assert.deepEqual(await delayedOwner, { status: 'silent', reason: 'check-in-progress' });
pause.restore();
assert.equal(requests, 1);
releaseFetch(response(remoteRelease()));
assert.equal((await successor).status, 'update_available');
assert.equal(requests, 1);
});
test('an overlapping check reads the last-good candidate while another process refreshes it', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
let requests = 0;
let releaseRefresh;
let markRefreshStarted;
const refreshStarted = new Promise((resolve) => { markRefreshStarted = resolve; });
const fetchImpl = async () => {
requests += 1;
if (requests === 1) return response(remoteRelease());
markRefreshStarted();
return new Promise((resolve) => { releaseRefresh = resolve; });
};
const first = await checkForUpdate(options(testFixture, fetchImpl));
const refreshOptions = options(testFixture, fetchImpl, {
now: () => baseTime + (73 * 60 * 60 * 1_000),
});
const refresh = checkForUpdate(refreshOptions);
await refreshStarted;
const overlapping = await checkForUpdate(refreshOptions);
assert.equal(overlapping.status, 'update_available');
assert.equal(overlapping.eventKey, first.eventKey);
assert.equal(requests, 2);
releaseRefresh(response(remoteRelease()));
assert.equal((await refresh).status, 'update_available');
});
test('acknowledgement waits briefly for an in-flight refresh instead of losing the notice', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const offered = await checkForUpdate(options(
testFixture,
async () => response(remoteRelease()),
));
let releaseRefresh;
let markRefreshStarted;
const refreshStarted = new Promise((resolve) => { markRefreshStarted = resolve; });
const refresh = checkForUpdate(options(testFixture, async () => {
markRefreshStarted();
return new Promise((resolve) => { releaseRefresh = resolve; });
}, {
now: () => baseTime + (73 * 60 * 60 * 1_000),
}));
await refreshStarted;
const acknowledgement = acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: offered.eventKey,
now: () => baseTime + (73 * 60 * 60 * 1_000) + 1,
});
await new Promise((resolve) => setTimeout(resolve, 25));
releaseRefresh(response(remoteRelease()));
await refresh;
assert.deepEqual(await acknowledgement, {
status: 'acknowledged',
eventKey: offered.eventKey,
});
assert.deepEqual(await checkForUpdate(options(testFixture, async () => response(remoteRelease()), {
now: () => baseTime + (74 * 60 * 60 * 1_000),
})), { status: 'silent', reason: 'already-notified' });
});
test('a last-good notice remains acknowledgeable after the refresh commits a new candidate', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const firstRelease = remoteReleaseForVersion('2.16.0', 'b'.repeat(64));
const secondRelease = remoteReleaseForVersion('2.17.0', 'c'.repeat(64));
const offered = await checkForUpdate(options(
testFixture,
async () => response(firstRelease),
));
let releaseRefresh;
let markRefreshStarted;
const refreshStarted = new Promise((resolve) => { markRefreshStarted = resolve; });
const refresh = checkForUpdate(options(testFixture, async () => {
markRefreshStarted();
return new Promise((resolve) => { releaseRefresh = resolve; });
}, {
now: () => baseTime + (73 * 60 * 60 * 1_000),
}));
await refreshStarted;
const visibleDuringRefresh = await checkForUpdate(options(testFixture, async () => {
assert.fail('an overlapping reader must not start another network request');
}, {
now: () => baseTime + (73 * 60 * 60 * 1_000),
}));
assert.equal(visibleDuringRefresh.eventKey, offered.eventKey);
const acknowledgement = acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: visibleDuringRefresh.eventKey,
});
await new Promise((resolve) => setTimeout(resolve, 25));
releaseRefresh(response(secondRelease));
assert.equal((await refresh).latestVersion, '2.17.0');
assert.deepEqual(await acknowledgement, {
status: 'acknowledged',
eventKey: offered.eventKey,
});
assert.deepEqual(await checkForUpdate(options(testFixture, async () => response(firstRelease), {
now: () => baseTime + (146 * 60 * 60 * 1_000),
})), { status: 'silent', reason: 'already-notified' });
});
test('acknowledgement retry budget uses a monotonic clock', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const offered = await checkForUpdate(options(
testFixture,
async () => response(remoteRelease()),
));
writePendingOperation(testFixture, { generation: 2n });
const ticks = [100, 1_301];
const started = Date.now();
assert.deepEqual(await acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: offered.eventKey,
monotonicNow: () => ticks.shift() ?? 1_301,
}), { status: 'silent', reason: 'check-in-progress' });
assert.ok(Date.now() - started < 200, 'an exhausted monotonic budget should not sleep on wall time');
});
test('a delayed allocator cannot reuse a generation after its reservation name was taken', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const firstRelease = remoteReleaseForVersion('2.16.0', 'b'.repeat(64));
const secondRelease = remoteReleaseForVersion('2.17.0', 'c'.repeat(64));
const offered = await checkForUpdate(options(
testFixture,
async () => response(firstRelease),
));
const pause = pauseMkdirOnce(operationPath(testFixture, 'reserved', 2n));
t.after(() => pause.restore());
const acknowledgement = acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: offered.eventKey,
});
await pause.reached;
assert.equal((await checkForUpdate(options(testFixture, async () => response(secondRelease), {
now: () => baseTime + (73 * 60 * 60 * 1_000),
}))).latestVersion, '2.17.0');
pause.release();
assert.deepEqual(await acknowledgement, { status: 'acknowledged', eventKey: offered.eventKey });
pause.restore();
assert.equal(fs.existsSync(operationPath(testFixture, 'reserved', 2n)), true);
assert.equal(fs.existsSync(operationPath(testFixture, 'reserved', 3n)), true);
assert.deepEqual(await checkForUpdate(options(testFixture, async () => response(firstRelease), {
now: () => baseTime + (146 * 60 * 60 * 1_000),
})), { status: 'silent', reason: 'already-notified' });
});
test('an oversized generation name cannot poison future allocation', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
fs.mkdirSync(stateDirectory(testFixture), { recursive: true });
fs.mkdirSync(path.join(stateDirectory(testFixture), `reserved-${'9'.repeat(246)}`));
let requests = 0;
const fetchImpl = async () => {
requests += 1;
return response(remoteRelease());
};
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
assert.equal(requests, 1);
assert.ok(fs.existsSync(operationPath(testFixture, 'reserved', 1n)));
});
test('a lower generation stalled after reservation cannot commit behind a newer generation', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const firstRelease = remoteReleaseForVersion('2.16.0', 'b'.repeat(64));
const secondRelease = remoteReleaseForVersion('2.17.0', 'c'.repeat(64));
const offered = await checkForUpdate(options(
testFixture,
async () => response(firstRelease),
));
const pause = pauseMkdirOnce(operationPath(testFixture, 'pending', 2n));
t.after(() => pause.restore());
const acknowledgement = acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: offered.eventKey,
});
await pause.reached;
assert.equal(fs.existsSync(operationPath(testFixture, 'reserved', 2n)), true);
assert.equal((await checkForUpdate(options(testFixture, async () => response(secondRelease), {
now: () => baseTime + (73 * 60 * 60 * 1_000),
}))).latestVersion, '2.17.0');
pause.release();
assert.deepEqual(await acknowledgement, { status: 'acknowledged', eventKey: offered.eventKey });
pause.restore();
assert.equal(fs.existsSync(operationPath(testFixture, 'committed', 2n)), false);
assert.equal(fs.existsSync(operationPath(testFixture, 'cancelled', 2n)), true);
assert.equal(fs.existsSync(operationPath(testFixture, 'reserved', 3n)), true);
assert.equal(fs.existsSync(operationPath(testFixture, 'reserved', 4n)), true);
assert.deepEqual(await checkForUpdate(options(testFixture, async () => response(firstRelease), {
now: () => baseTime + (146 * 60 * 60 * 1_000),
})), { status: 'silent', reason: 'already-notified' });
});
test('acknowledgement fences an expired refresh and the resumed owner cannot erase it', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const offered = await checkForUpdate(options(
testFixture,
async () => response(remoteRelease()),
));
let resumeRefresh;
let markRefreshStarted;
const refreshStarted = new Promise((resolve) => { markRefreshStarted = resolve; });
const refresh = checkForUpdate(options(testFixture, async () => {
markRefreshStarted();
return new Promise((resolve) => { resumeRefresh = resolve; });
}, {
now: () => baseTime + (73 * 60 * 60 * 1_000),
timeoutMs: 2_000,
}));
await refreshStarted;
const refreshPending = operationPath(testFixture, 'pending', 2n);
const staleTime = new Date(Date.now() - 60_000);
fs.utimesSync(path.join(refreshPending, 'owner.json'), staleTime, staleTime);
assert.deepEqual(await acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: offered.eventKey,
now: () => baseTime + (73 * 60 * 60 * 1_000) + 1,
}), { status: 'acknowledged', eventKey: offered.eventKey });
resumeRefresh(response(remoteRelease()));
assert.deepEqual(await refresh, { status: 'silent', reason: 'check-in-progress' });
assert.deepEqual(await checkForUpdate(options(testFixture, async () => response(remoteRelease()), {
now: () => baseTime + (71 * 60 * 60 * 1_000),
})), { status: 'silent', reason: 'already-notified' });
});
for (const pausePoint of ['pending owner creation', 'state temp creation']) {
test(`acknowledgement retries when fenced during ${pausePoint}`, async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const offered = await checkForUpdate(options(
testFixture,
async () => response(remoteRelease()),
));
const pending = operationPath(testFixture, 'pending', 2n);
const pause = pauseOpenOnce((target) => {
if (pausePoint === 'pending owner creation') {
return path.resolve(target) === path.join(pending, 'owner.json');
}
return path.dirname(path.resolve(target)) === pending
&& /^\.state\..+\.tmp$/.test(path.basename(target));
});
t.after(() => pause.restore());
const acknowledgement = acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: offered.eventKey,
});
await pause.reached;
const staleTime = new Date(Date.now() - 60_000);
fs.utimesSync(
pausePoint === 'pending owner creation' ? pending : path.join(pending, 'owner.json'),
staleTime,
staleTime,
);
assert.equal((await checkForUpdate(options(
testFixture,
async () => response(remoteRelease()),
{ now: () => baseTime + (73 * 60 * 60 * 1_000) },
))).status, 'update_available');
assert.equal(fs.existsSync(operationPath(testFixture, 'fenced', 2n)), true);
pause.release();
assert.deepEqual(await acknowledgement, {
status: 'acknowledged',
eventKey: offered.eventKey,
});
pause.restore();
});
}
test('a stale corrupt operation is fenced without exposing a failure', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const staleTime = new Date(Date.now() - 60_000);
const pending = writePendingOperation(testFixture, {
owner: 'corrupt-owner',
modifiedAt: staleTime,
});
const result = await checkForUpdate(options(
testFixture,
async () => response(remoteRelease()),
));
assert.equal(result.status, 'update_available');
assert.equal(fs.existsSync(pending), false);
assert.equal(fs.existsSync(operationPath(testFixture, 'fenced', 1n)), true);
});
test('malformed active-claim shapes recover after the hard lease', async () => {
for (const shape of ['file', 'symlink', 'owner-directory']) {
const testFixture = fixture();
try {
const directory = stateDirectory(testFixture);
const activeClaim = path.join(directory, 'active-claim');
fs.mkdirSync(directory, { recursive: true });
if (shape === 'file') {
fs.writeFileSync(activeClaim, 'corrupt claim');
} else if (shape === 'symlink') {
fs.symlinkSync(path.join(testFixture.root, 'missing-claim-target'), activeClaim);
} else {
fs.mkdirSync(path.join(activeClaim, 'owner.json'), { recursive: true });
}
let requests = 0;
const fetchImpl = async () => {
requests += 1;
return response(remoteRelease());
};
assert.deepEqual(await checkForUpdate(options(testFixture, fetchImpl)), {
status: 'silent',
reason: 'check-in-progress',
}, `${shape} should retain a fresh hard lease`);
assert.equal(requests, 0);
const staleTime = new Date(Date.now() - 60_000);
if (shape === 'symlink') fs.lutimesSync(activeClaim, staleTime, staleTime);
else if (shape === 'owner-directory') {
fs.utimesSync(path.join(activeClaim, 'owner.json'), staleTime, staleTime);
} else fs.utimesSync(activeClaim, staleTime, staleTime);
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
assert.equal(requests, 1, `${shape} should be retired after its hard lease`);
} finally {
fs.rmSync(testFixture.root, { recursive: true, force: true });
}
}
});
test('a delayed corrupt-claim retirement cannot move a successor active claim', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const directory = stateDirectory(testFixture);
const activeClaim = path.join(directory, 'active-claim');
fs.mkdirSync(activeClaim, { recursive: true });
const staleTime = new Date(Date.now() - 60_000);
fs.utimesSync(activeClaim, staleTime, staleTime);
let firstRetirement = null;
const pause = pauseRenameMatchingOnce((source, destination) => {
if (path.resolve(source) !== activeClaim
|| !path.basename(path.dirname(destination)).startsWith('retired-claim-corrupt-')) {
return false;
}
firstRetirement = destination;
return true;
});
t.after(() => pause.restore());
const delayed = checkForUpdate(options(
testFixture,
async () => response(remoteRelease()),
));
await pause.reached;
fs.utimesSync(activeClaim, staleTime, staleTime);
fs.utimesSync(
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
staleTime,
staleTime,
);
assert.equal((await checkForUpdate(options(
testFixture,
async () => response(remoteRelease()),
))).status, 'update_available');
assert.equal(
JSON.parse(fs.readFileSync(path.join(activeClaim, 'owner.json'), 'utf8')).generation,
'2',
);
pause.release();
assert.equal((await delayed).status, 'update_available');
pause.restore();
assert.equal(firstRetirement !== null, true);
assert.equal(fs.existsSync(activeClaim), true);
assert.equal(
JSON.parse(fs.readFileSync(path.join(activeClaim, 'owner.json'), 'utf8')).generation,
'2',
);
});
test('repeated corrupt claim contents receive distinct retirement identities', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const directory = stateDirectory(testFixture);
const activeClaim = path.join(directory, 'active-claim');
fs.mkdirSync(directory, { recursive: true });
fs.writeFileSync(activeClaim, 'same corrupt claim');
const staleTime = new Date(Date.now() - 60_000);
fs.utimesSync(activeClaim, staleTime, staleTime);
let requests = 0;
const fetchImpl = async () => {
requests += 1;
return response(remoteRelease());
};
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
fs.renameSync(activeClaim, path.join(directory, 'test-completed-active-claim'));
fs.writeFileSync(activeClaim, 'same corrupt claim');
fs.utimesSync(activeClaim, staleTime, staleTime);
assert.equal((await checkForUpdate(options(testFixture, fetchImpl, {
now: () => baseTime + (73 * 60 * 60 * 1_000),
}))).status, 'update_available');
assert.equal(requests, 2);
assert.equal(
fs.readdirSync(directory).filter((name) => name.startsWith('retired-claim-corrupt-')).length,
2,
);
});
test('corrupt claim retirement preserves distinct 64-bit inode identities', async (t) => {
const testFixture = fixture();
const originalLstat = fsPromises.lstat;
t.after(() => {
fsPromises.lstat = originalLstat;
fs.rmSync(testFixture.root, { recursive: true, force: true });
});
const directory = stateDirectory(testFixture);
const activeClaim = path.join(directory, 'active-claim');
fs.mkdirSync(directory, { recursive: true });
fs.writeFileSync(activeClaim, 'corrupt claim');
const staleTime = new Date(Date.now() - 60_000);
fs.utimesSync(activeClaim, staleTime, staleTime);
let instance = 1n;
fsPromises.lstat = async (target, ...args) => {
const metadata = await originalLstat(target, ...args);
const resolved = path.resolve(target);
const isCorruptClaimInstance = resolved === path.resolve(activeClaim)
|| (path.basename(resolved) === 'active'
&& path.basename(path.dirname(resolved)).startsWith('retired-claim-corrupt-'));
if (!isCorruptClaimInstance || !metadata.isFile()) return metadata;
const inode = 9_007_199_254_740_992n + (instance - 1n);
return args[0]?.bigint
? metadataWithOverrides(metadata, {
ino: inode,
birthtimeNs: 1_000n,
ctimeNs: 2_000n,
})
: metadataWithOverrides(metadata, {
ino: Number(inode),
birthtimeMs: 1,
ctimeMs: 2,
});
};
let requests = 0;
const fetchImpl = async () => {
requests += 1;
return response(remoteRelease());
};
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
fs.renameSync(activeClaim, path.join(directory, 'test-completed-active-claim'));
fs.writeFileSync(activeClaim, 'corrupt claim');
fs.utimesSync(activeClaim, staleTime, staleTime);
instance = 2n;
assert.equal((await checkForUpdate(options(testFixture, fetchImpl, {
now: () => baseTime + (73 * 60 * 60 * 1_000),
}))).status, 'update_available');
assert.equal(requests, 2);
assert.equal(
fs.readdirSync(directory).filter((name) => name.startsWith('retired-claim-corrupt-')).length,
2,
);
});
test('a forged retirement symlink cannot move an active claim outside its cache partition', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
let requests = 0;
const fetchImpl = async () => {
requests += 1;
return response(remoteRelease());
};
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
const outside = path.join(testFixture.root, 'outside');
fs.mkdirSync(outside);
fs.symlinkSync(outside, path.join(stateDirectory(testFixture), 'retired-claim-1'));
const refreshOptions = options(testFixture, fetchImpl, {
now: () => baseTime + (73 * 60 * 60 * 1_000),
});
assert.equal((await checkForUpdate(refreshOptions)).status, 'update_available');
assert.deepEqual(fs.readdirSync(outside), []);
assert.equal(requests, 1);
fs.unlinkSync(path.join(stateDirectory(testFixture), 'retired-claim-1'));
assert.equal((await checkForUpdate(refreshOptions)).status, 'update_available');
assert.deepEqual(fs.readdirSync(outside), []);
assert.equal(requests, 2);
});
test('an expired operation is fenced even when its PID was reused by a live process', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const staleTime = new Date(Date.now() - 60_000);
const pending = writePendingOperation(testFixture, { modifiedAt: staleTime });
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.equal(result.status, 'update_available');
assert.equal(requests, 1);
assert.equal(fs.existsSync(pending), false);
assert.equal(fs.existsSync(operationPath(testFixture, 'fenced', 1n)), true);
});
test('a corrupt future-dated operation cannot suppress checks indefinitely', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const futureTime = new Date(Date.now() + (365 * 24 * 60 * 60 * 1_000));
const pending = writePendingOperation(testFixture, {
owner: 'corrupt-owner',
modifiedAt: futureTime,
});
const result = await checkForUpdate(options(
testFixture,
async () => response(remoteRelease()),
));
assert.equal(result.status, 'update_available');
assert.equal(fs.existsSync(pending), false);
assert.equal(fs.existsSync(operationPath(testFixture, 'fenced', 1n)), true);
});
test('a newly-created operation with slight clock skew is never fenced as stale', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const nearFuture = new Date(Date.now() + 1_000);
const pending = writePendingOperation(testFixture, {
owner: 'writer-has-not-finished-the-record',
modifiedAt: nearFuture,
});
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.deepEqual(result, { status: 'silent', reason: 'check-in-progress' });
assert.equal(requests, 0);
assert.equal(fs.existsSync(pending), true);
});
test('a fenced stale owner cannot overwrite a newer committed candidate after resuming', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
let resumeOldOwner;
let markOldStarted;
const oldStarted = new Promise((resolve) => { markOldStarted = resolve; });
const oldCheck = checkForUpdate(options(testFixture, async () => {
markOldStarted();
return new Promise((resolve) => { resumeOldOwner = resolve; });
}, { timeoutMs: 2_000 }));
await oldStarted;
const oldPending = operationPath(testFixture, 'pending', 1n);
const staleTime = new Date(Date.now() - 60_000);
fs.utimesSync(path.join(oldPending, 'owner.json'), staleTime, staleTime);
const newer = await checkForUpdate(options(
testFixture,
async () => response(remoteReleaseForVersion('3.0.0', 'c'.repeat(64))),
));
assert.equal(newer.status, 'update_available');
assert.equal(newer.latestVersion, '3.0.0');
resumeOldOwner(response(remoteReleaseForVersion('2.16.0', 'b'.repeat(64))));
assert.deepEqual(await oldCheck, { status: 'silent', reason: 'check-in-progress' });
const persisted = JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8'));
assert.equal(persisted.candidate.version, '3.0.0');
assert.equal(fs.existsSync(operationPath(testFixture, 'fenced', 1n)), true);
});
test('different installed versions share no acknowledgement or cache-reset state', async (t) => {
const testFixture = fixture('2.15.0');
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const olderReleasePath = path.join(testFixture.root, 'older-skill-release.json');
writeJson(olderReleasePath, localRelease('2.14.0'));
let requests = 0;
const fetchImpl = async () => {
requests += 1;
return response(remoteRelease());
};
const newerInstall = await checkForUpdate(options(testFixture, fetchImpl));
assert.equal(newerInstall.status, 'update_available');
assert.deepEqual(await acknowledgeUpdate({
releasePath: testFixture.releasePath,
cacheDirectory: testFixture.cacheDirectory,
eventKey: newerInstall.eventKey,
now: () => baseTime + 1_000,
}), { status: 'acknowledged', eventKey: newerInstall.eventKey });
const olderInstallOptions = options(testFixture, fetchImpl, { releasePath: olderReleasePath });
const olderInstall = await checkForUpdate(olderInstallOptions);
assert.equal(olderInstall.status, 'update_available');
assert.equal(olderInstall.installedVersion, '2.14.0');
assert.deepEqual(await checkForUpdate(options(testFixture, fetchImpl)), {
status: 'silent',
reason: 'already-notified',
});
const olderAgain = await checkForUpdate(olderInstallOptions);
assert.equal(olderAgain.status, 'update_available');
assert.equal(requests, 2, 'each installed version should retain its own fresh cache');
assert.equal(fs.existsSync(statePath(testFixture, '2.15.0')), true);
assert.equal(fs.existsSync(statePath(testFixture, '2.14.0')), true);
});
test('an identity mismatch is rejected silently and never cached as a candidate', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const result = await checkForUpdate(options(
testFixture,
async () => response(remoteRelease({ skillId: 'different-skill' })),
));
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' });
const state = JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8'));
assert.equal(Object.hasOwn(state, 'candidate'), false);
});
test('release notes must byte-match the exact trusted GitHub URL', async () => {
const base = 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0';
for (const releaseNotes of [
'https://github.com:443/tt-a1i/archify/releases/tag/v2.16.0',
'https://github.com:444/tt-a1i/archify/releases/tag/v2.16.0',
`${base}?`,
`${base}#`,
`${base}?source=manifest`,
'https://GITHUB.COM/tt-a1i/archify/releases/tag/v2.16.0',
]) {
const testFixture = fixture();
try {
const result = await checkForUpdate(options(
testFixture,
async () => response(remoteRelease({ releaseNotes })),
));
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' }, releaseNotes);
} finally {
fs.rmSync(testFixture.root, { recursive: true, force: true });
}
}
});
test('malformed release notes are classified as an invalid manifest', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const result = await checkForUpdate(options(
testFixture,
async () => response(remoteRelease({ releaseNotes: 'not-a-url' })),
));
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' });
});
test('publishedAt must use canonical UTC seconds and a real calendar date', async () => {
for (const publishedAt of [
'2026-08-28T15:00:00+08:00',
'2026-08-28',
'2026-02-30T00:00:00Z',
]) {
const testFixture = fixture();
try {
const result = await checkForUpdate(options(
testFixture,
async () => response(remoteRelease({ publishedAt })),
));
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' }, publishedAt);
} finally {
fs.rmSync(testFixture.root, { recursive: true, force: true });
}
}
});
test('stable candidates cannot use prerelease or build metadata', async () => {
for (const version of ['2.16.0-dev.1', '2.16.0+build.1']) {
const testFixture = fixture();
try {
const result = await checkForUpdate(options(
testFixture,
async () => response(remoteReleaseForVersion(version)),
));
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' }, version);
} finally {
fs.rmSync(testFixture.root, { recursive: true, force: true });
}
}
});
test('an invalid local identity fails before any network disclosure', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
writeJson(testFixture.releasePath, {
...localRelease(),
source: { repository: 'https://example.com/untrusted/archify' },
});
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.deepEqual(result, { status: 'silent', reason: 'invalid-local-release' });
assert.equal(requests, 0);
});
test('a bounded timeout fails silently without retrying', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
let requests = 0;
const fetchImpl = (_url, init) => new Promise((_resolve, reject) => {
requests += 1;
init.signal.addEventListener('abort', () => {
const error = new Error('aborted');
error.name = 'AbortError';
reject(error);
}, { once: true });
});
const result = await checkForUpdate(options(testFixture, fetchImpl, { timeoutMs: 10 }));
assert.deepEqual(result, { status: 'silent', reason: 'check-failed' });
assert.equal(requests, 1);
});
test('non-success responses cancel their unread body before failing silently', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
let cancelled = false;
const result = await checkForUpdate(options(testFixture, async () => ({
status: 503,
headers: new Headers(),
body: {
async cancel() {
cancelled = true;
},
},
})));
assert.deepEqual(result, { status: 'silent', reason: 'check-failed' });
assert.equal(cancelled, true);
});
test('media-type and declared-size rejection cancel unread response bodies', async () => {
for (const headers of [
{ 'content-type': 'text/html' },
{ 'content-type': 'application/json', 'content-length': String((32 * 1024) + 1) },
]) {
const testFixture = fixture();
let cancelled = false;
try {
const result = await checkForUpdate(options(testFixture, async () => ({
status: 200,
headers: new Headers(headers),
body: {
async cancel() {
cancelled = true;
},
},
})));
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' });
assert.equal(cancelled, true);
} finally {
fs.rmSync(testFixture.root, { recursive: true, force: true });
}
}
});
test('response stream failures are classified as network failures', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const result = await checkForUpdate(options(testFixture, async () => ({
status: 200,
headers: new Headers({ 'content-type': 'application/json' }),
body: {
getReader() {
return {
async read() {
throw new TypeError('socket closed while reading');
},
releaseLock() {},
};
},
},
})));
assert.deepEqual(result, { status: 'silent', reason: 'check-failed' });
});
test('invalid UTF-8 is classified as an invalid manifest rather than a network failure', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const result = await checkForUpdate(options(
testFixture,
async () => new Response(new Uint8Array([0xff]), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
));
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' });
});
test('chunked responses above 32 KiB are rejected instead of fully trusted', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
const oversized = `${JSON.stringify(remoteRelease())}${' '.repeat(33 * 1024)}`;
const result = await checkForUpdate(options(
testFixture,
async () => response(oversized),
));
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' });
});
test('a poisoned far-future cache timestamp cannot suppress checks indefinitely', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
await checkForUpdate(options(testFixture, async () => response(remoteRelease())));
const cacheStatePath = statePath(testFixture);
const state = JSON.parse(fs.readFileSync(cacheStatePath, 'utf8'));
state.check.nextCheckAt = '9999-12-31T23:59:59.000Z';
writeJson(cacheStatePath, state);
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteRelease());
}));
assert.equal(result.status, 'update_available');
assert.equal(requests, 1);
});
test('a semantically corrupt cached candidate discards legacy validators and rebuilds unconditionally', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
await checkForUpdate(options(testFixture, async () => response(remoteRelease())));
const cacheStatePath = statePath(testFixture);
const state = JSON.parse(fs.readFileSync(cacheStatePath, 'utf8'));
state.candidate.targetDigest = 'sha256:corrupt';
state.check.etag = '"legacy-tracker"';
state.check.nextCheckAt = '9999-12-31T23:59:59.000Z';
writeJson(cacheStatePath, state);
let conditionalHeader = 'not-observed';
const result = await checkForUpdate(options(testFixture, async (_url, init) => {
conditionalHeader = init.headers['if-none-match'];
return response(remoteRelease());
}));
assert.equal(result.status, 'update_available');
assert.equal(conditionalHeader, undefined);
assert.equal(
JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).candidate.targetDigest,
result.targetDigest,
);
});
test('a newer cached candidate without offered or acknowledged provenance is rebuilt', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
writeJson(statePath(testFixture), {
schemaVersion: 1,
skillId: 'archify',
installedVersion: '2.15.0',
check: {
nextCheckAt: new Date(baseTime + (24 * 60 * 60 * 1_000)).toISOString(),
consecutiveFailures: 0,
},
notification: {
offeredDigests: [],
acknowledgedDigests: [],
},
candidate: {
version: '2.16.0',
targetDigest: `sha256:${'b'.repeat(64)}`,
severity: 'normal',
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0',
},
});
let requests = 0;
const result = await checkForUpdate(options(testFixture, async () => {
requests += 1;
return response(remoteReleaseForVersion('2.15.0', 'd'.repeat(64)));
}));
assert.deepEqual(result, { status: 'silent', reason: 'current' });
assert.equal(requests, 1, 'semantic corruption must not produce an unacknowledgeable cached notice');
});
test('corrupt cache is rebuilt without exposing an error to the user', async (t) => {
const testFixture = fixture();
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
fs.mkdirSync(stateDirectory(testFixture), { recursive: true });
fs.mkdirSync(path.dirname(statePath(testFixture)), { recursive: true });
fs.writeFileSync(statePath(testFixture), '{not-json');
const result = await checkForUpdate(options(
testFixture,
async () => response(remoteRelease({ version: '2.15.0' })),
));
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' });
assert.doesNotThrow(() => JSON.parse(
fs.readFileSync(statePath(testFixture), 'utf8'),
));
});
test('disabled CLI returns one silent JSON line and never needs the network', () => {
const result = spawnSync(process.execPath, [checkerPath], {
cwd: skillRoot,
encoding: 'utf8',
env: { ...process.env, ARCHIFY_UPDATE_CHECK_DISABLED: '1' },
});
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(JSON.parse(result.stdout), { status: 'silent', reason: 'disabled' });
assert.equal(result.stdout.trim().split('\n').length, 1);
});
test('CLI acknowledgement emits the documented one-line success schema', async (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-update-cli-ack-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const home = path.join(root, 'home');
const xdg = path.join(root, 'xdg-cache');
const localData = path.join(root, 'local-data');
for (const directory of [home, xdg, localData]) fs.mkdirSync(directory, { recursive: true });
const cacheDirectory = process.platform === 'win32'
? path.join(localData, 'archify-skill')
: process.platform === 'darwin'
? path.join(home, 'Library', 'Caches', 'archify-skill')
: path.join(xdg, 'archify-skill');
const releasePath = path.join(skillRoot, 'skill-release.json');
const installedRelease = JSON.parse(fs.readFileSync(releasePath, 'utf8'));
const [major, minor, patch] = parseSemver(installedRelease.version).core;
const candidateVersion = `${major}.${minor}.${BigInt(patch) + 1n}`;
const offered = await checkForUpdate({
releasePath,
cacheDirectory,
fetchImpl: async () => response(remoteReleaseForVersion(candidateVersion)),
now: () => baseTime,
random: () => 0.5,
timeoutMs: 50,
});
assert.equal(offered.status, 'update_available');
const acknowledgement = spawnSync(process.execPath, [checkerPath, '--ack', offered.eventKey], {
cwd: skillRoot,
encoding: 'utf8',
env: {
...process.env,
HOME: home,
USERPROFILE: home,
XDG_CACHE_HOME: xdg,
LOCALAPPDATA: localData,
},
});
assert.equal(acknowledgement.status, 0, acknowledgement.stderr);
assert.deepEqual(JSON.parse(acknowledgement.stdout), {
status: 'acknowledged',
eventKey: offered.eventKey,
});
assert.equal(acknowledgement.stdout.trim().split('\n').length, 1);
});
test('CLI entry detection survives a realpath or symlink alias', (t) => {
const aliasRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-update-cli-alias-'));
t.after(() => fs.rmSync(aliasRoot, { recursive: true, force: true }));
const aliasPath = path.join(aliasRoot, 'check-update-alias.mjs');
try {
fs.symlinkSync(checkerPath, aliasPath);
} catch (error) {
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
t.skip(`symlinks unavailable: ${error.code}`);
return;
}
throw error;
}
const result = spawnSync(process.execPath, [aliasPath], {
cwd: aliasRoot,
encoding: 'utf8',
env: { ...process.env, ARCHIFY_UPDATE_CHECK_DISABLED: '1' },
});
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(JSON.parse(result.stdout), { status: 'silent', reason: 'disabled' });
});
test('notifier source has no process execution or remote-origin override surface', () => {
const checkerSource = fs.readFileSync(checkerPath, 'utf8');
const contractSource = fs.readFileSync(contractPath, 'utf8');
const combinedSource = `${checkerSource}\n${contractSource}`;
assert.doesNotMatch(combinedSource, /(?:node:)?child_process/);
assert.doesNotMatch(combinedSource, /(?:^|[^\w.])(?:spawn|exec|execFile|fork)(?:Sync)?\s*\(/m);
assert.doesNotMatch(contractSource, /\bfetch\s*\(/);
const environmentReads = [...combinedSource.matchAll(/process\.env\.([A-Z0-9_]+)/g)]
.map((match) => match[1])
.sort();
assert.deepEqual(environmentReads, [
'ARCHIFY_UPDATE_CHECK_DISABLED',
'LOCALAPPDATA',
'XDG_CACHE_HOME',
]);
assert.doesNotMatch(combinedSource, /updateCommand/);
});