diff --git a/src/tui/shared/state-write.ts b/src/tui/shared/state-write.ts new file mode 100644 index 0000000..17af437 --- /dev/null +++ b/src/tui/shared/state-write.ts @@ -0,0 +1,29 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { Selection } from './types.js'; + +export async function writeSelection( + filePath: string, + projectDir: string, + selection: Selection +): Promise { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + + const payload = { + version: 1, + updated_at: new Date().toISOString(), + project_dir: projectDir, + device: selection.device, + selection: { + kind: selection.pou.kind, + name: selection.pou.name, + path: selection.pou.relPath, + abs_path: selection.pou.absPath, + }, + viewer_line: selection.viewerLine, + }; + + const tmp = `${filePath}.${process.pid}.tmp`; + await fs.writeFile(tmp, JSON.stringify(payload, null, 2), 'utf8'); + await fs.rename(tmp, filePath); +} diff --git a/tests/tui/state-write.test.ts b/tests/tui/state-write.test.ts new file mode 100644 index 0000000..3b14089 --- /dev/null +++ b/tests/tui/state-write.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from 'vitest'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import * as os from 'os'; +import { writeSelection } from '../../src/tui/shared/state-write.ts'; +import { Selection } from '../../src/tui/shared/types.ts'; + +async function tmpDir(): Promise { + return fs.mkdtemp(path.join(os.tmpdir(), 'phobics-tui-')); +} + +const sampleSelection: Selection = { + device: 'CodesysRpi', + pou: { + name: 'FB_Test', + kind: 'FB', + relPath: 'Plc Logic/Application/FB_Test.st', + absPath: '/abs/Plc Logic/Application/FB_Test.st', + loc: 87, + mtimeMs: 0, + }, + viewerLine: 12, +}; + +describe('writeSelection', () => { + it('writes a JSON file atomically with v1 envelope', async () => { + const dir = await tmpDir(); + const target = path.join(dir, 'tui-state.json'); + await writeSelection(target, '/abs/project', sampleSelection); + + const text = await fs.readFile(target, 'utf8'); + const parsed = JSON.parse(text); + expect(parsed.version).toBe(1); + expect(parsed.project_dir).toBe('/abs/project'); + expect(parsed.device).toBe('CodesysRpi'); + expect(parsed.selection.name).toBe('FB_Test'); + expect(parsed.selection.kind).toBe('FB'); + expect(parsed.viewer_line).toBe(12); + expect(typeof parsed.updated_at).toBe('string'); + expect(() => new Date(parsed.updated_at)).not.toThrow(); + }); + + it('creates parent directories as needed', async () => { + const dir = await tmpDir(); + const target = path.join(dir, 'a', 'b', 'tui-state.json'); + await writeSelection(target, '/abs/project', sampleSelection); + const stat = await fs.stat(target); + expect(stat.isFile()).toBe(true); + }); + + it('does not leave the .tmp file behind on success', async () => { + const dir = await tmpDir(); + const target = path.join(dir, 'tui-state.json'); + await writeSelection(target, '/abs/project', sampleSelection); + const entries = await fs.readdir(dir); + expect(entries.filter((e) => e.endsWith('.tmp'))).toEqual([]); + }); +});