fix(qmd): write index.yml atomically
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ced6d8f119
commit
abface2293
3 files changed
+23
-10
No files matched your search
@@ -1,10 +1,10 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import * as YAML from './fixtures/yaml-shim.mjs';
|
import * as YAML from './fixtures/yaml-shim.mjs';
|
||||||
import { collectionName, deviceEnv, idleMinutes, qmdConfigPath, writePrivateJson, readJson, runQmdCli, MODELS } from '../../shared/qmd/qmd-lib.mjs';
|
import { collectionName, deviceEnv, idleMinutes, qmdConfigPath, writePrivateJson, writeTextAtomic, readJson, runQmdCli, MODELS } from '../../shared/qmd/qmd-lib.mjs';
|
||||||
import { setModels, unsetModels, previousModels, restoreModels } from '../../shared/qmd/qmd-config.mjs';
|
import { setModels, unsetModels, previousModels, restoreModels } from '../../shared/qmd/qmd-config.mjs';
|
||||||
|
|
||||||
test('collection name is stable, prefixed, and path-safe', () => {
|
test('collection name is stable, prefixed, and path-safe', () => {
|
||||||
@@ -93,3 +93,12 @@ test('restoreModels writes the previous block back and keeps comments and other
|
|||||||
assert.match(restored, /collections:/);
|
assert.match(restored, /collections:/);
|
||||||
assert.doesNotMatch(restored, /Qwen/);
|
assert.doesNotMatch(restored, /Qwen/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('writeTextAtomic replaces the file through a temp file in the same directory', () => {
|
||||||
|
const dir = join(mkdtempSync(join(tmpdir(), 'qmd-lib-')), 'cfg');
|
||||||
|
const file = join(dir, 'index.yml');
|
||||||
|
writeTextAtomic(file, 'a: 1\n');
|
||||||
|
writeTextAtomic(file, 'b: 2\n');
|
||||||
|
assert.equal(readFileSync(file, 'utf8'), 'b: 2\n');
|
||||||
|
assert.deepEqual(readdirSync(dir), ['index.yml']);
|
||||||
|
});
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// shared/qmd/qmd-config.mjs — edits only the models: block of QMD's index.yml.
|
// shared/qmd/qmd-config.mjs — edits only the models: block of QMD's index.yml.
|
||||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { dirname, join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { pathToFileURL } from 'node:url';
|
import { pathToFileURL } from 'node:url';
|
||||||
import { MODELS, qmdConfigPath, qmdPackageDir } from './qmd-lib.mjs';
|
import { MODELS, qmdConfigPath, qmdPackageDir, writeTextAtomic } from './qmd-lib.mjs';
|
||||||
|
|
||||||
export function setModels(text, YAML) {
|
export function setModels(text, YAML) {
|
||||||
const document = YAML.parseDocument(text || '');
|
const document = YAML.parseDocument(text || '');
|
||||||
@@ -49,8 +49,7 @@ async function main(action, argument) {
|
|||||||
if (mapping === undefined) throw new Error('restore-models needs a JSON value');
|
if (mapping === undefined) throw new Error('restore-models needs a JSON value');
|
||||||
next = restoreModels(current, YAML, mapping);
|
next = restoreModels(current, YAML, mapping);
|
||||||
} else next = unsetModels(current, YAML);
|
} else next = unsetModels(current, YAML);
|
||||||
mkdirSync(dirname(path), { recursive: true });
|
writeTextAtomic(path, next);
|
||||||
writeFileSync(path, next);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
|
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
|
||||||
|
|||||||
@@ -54,14 +54,19 @@ export function readJson(path, fallback) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The state directory lives in the user profile; mode 0600 restricts the token on POSIX.
|
// Readers never see a half-written file: write a temp file next to the target, then rename it.
|
||||||
export function writePrivateJson(path, value) {
|
export function writeTextAtomic(path, text, options = {}) {
|
||||||
mkdirSync(dirname(path), { recursive: true });
|
mkdirSync(dirname(path), { recursive: true });
|
||||||
const temporary = `${path}.${process.pid}.tmp`;
|
const temporary = `${path}.${process.pid}.tmp`;
|
||||||
writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
|
writeFileSync(temporary, text, options);
|
||||||
renameSync(temporary, path);
|
renameSync(temporary, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The state directory lives in the user profile; mode 0600 restricts the token on POSIX.
|
||||||
|
export function writePrivateJson(path, value) {
|
||||||
|
writeTextAtomic(path, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
|
||||||
|
}
|
||||||
|
|
||||||
let cachedPackageDir;
|
let cachedPackageDir;
|
||||||
|
|
||||||
export function qmdPackageDir() {
|
export function qmdPackageDir() {
|
||||||
|
|||||||
Reference in new issue
Block a user