feat(state): add CJS reader for phobiCS-tui state file
readSelection(path) -> discriminated union of: - ok (the parsed v1 payload) - missing (file does not exist) - stale (updated_at older than FRESHNESS_MS = 60s) - invalid (bad JSON or unknown version) Lives outside src/tui/ because the MCP server (CJS) consumes it; the TUI itself only writes the file.
This commit is contained in:
parent
015bae6d65
commit
0e100610e5
2 changed files with 105 additions and 0 deletions
46
src/state-read.ts
Normal file
46
src/state-read.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import * as fs from 'fs';
|
||||
|
||||
export const FRESHNESS_MS = 60_000;
|
||||
|
||||
export interface SelectionPayload {
|
||||
version: 1;
|
||||
updated_at: string;
|
||||
project_dir: string;
|
||||
device: string;
|
||||
selection: {
|
||||
kind: string;
|
||||
name: string;
|
||||
path: string;
|
||||
abs_path: string;
|
||||
};
|
||||
viewer_line: number;
|
||||
}
|
||||
|
||||
export type ReadResult =
|
||||
| { status: 'ok'; payload: SelectionPayload }
|
||||
| { status: 'missing' }
|
||||
| { status: 'stale' }
|
||||
| { status: 'invalid'; reason: string };
|
||||
|
||||
export async function readSelection(filePath: string): Promise<ReadResult> {
|
||||
let text: string;
|
||||
try {
|
||||
text = await fs.promises.readFile(filePath, 'utf8');
|
||||
} catch {
|
||||
return { status: 'missing' };
|
||||
}
|
||||
let parsed: SelectionPayload;
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
} catch (err) {
|
||||
return { status: 'invalid', reason: (err as Error).message };
|
||||
}
|
||||
if (parsed.version !== 1) {
|
||||
return { status: 'invalid', reason: `unsupported version ${parsed.version}` };
|
||||
}
|
||||
const ageMs = Date.now() - new Date(parsed.updated_at).getTime();
|
||||
if (Number.isNaN(ageMs) || ageMs > FRESHNESS_MS) {
|
||||
return { status: 'stale' };
|
||||
}
|
||||
return { status: 'ok', payload: parsed };
|
||||
}
|
||||
59
tests/unit/state-read.test.ts
Normal file
59
tests/unit/state-read.test.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { readSelection, FRESHNESS_MS } from '../../src/state-read';
|
||||
|
||||
async function tmpFile(content: string): Promise<string> {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'phobics-read-'));
|
||||
const f = path.join(dir, 'tui-state.json');
|
||||
await fs.writeFile(f, content, 'utf8');
|
||||
return f;
|
||||
}
|
||||
|
||||
const FRESH = JSON.stringify({
|
||||
version: 1,
|
||||
updated_at: new Date().toISOString(),
|
||||
project_dir: '/p',
|
||||
device: 'D1',
|
||||
selection: {
|
||||
kind: 'FB',
|
||||
name: 'FB_Test',
|
||||
path: 'a/FB_Test.st',
|
||||
abs_path: '/abs/a/FB_Test.st',
|
||||
},
|
||||
viewer_line: 12,
|
||||
});
|
||||
|
||||
describe('readSelection', () => {
|
||||
it('returns the parsed payload when fresh', async () => {
|
||||
const f = await tmpFile(FRESH);
|
||||
const r = await readSelection(f);
|
||||
expect(r.status).toBe('ok');
|
||||
if (r.status === 'ok') {
|
||||
expect(r.payload.device).toBe('D1');
|
||||
expect(r.payload.selection.name).toBe('FB_Test');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns stale when updated_at is older than the freshness window', async () => {
|
||||
const old = JSON.stringify({
|
||||
...JSON.parse(FRESH),
|
||||
updated_at: new Date(Date.now() - FRESHNESS_MS - 5000).toISOString(),
|
||||
});
|
||||
const f = await tmpFile(old);
|
||||
const r = await readSelection(f);
|
||||
expect(r.status).toBe('stale');
|
||||
});
|
||||
|
||||
it('returns missing when the file does not exist', async () => {
|
||||
const r = await readSelection('/nonexistent/tui-state.json');
|
||||
expect(r.status).toBe('missing');
|
||||
});
|
||||
|
||||
it('returns invalid when the JSON is malformed', async () => {
|
||||
const f = await tmpFile('not json');
|
||||
const r = await readSelection(f);
|
||||
expect(r.status).toBe('invalid');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue