|
|
@@ -3,6 +3,8 @@ import * as fs from 'node:fs';
|
|
|
import * as os from 'node:os';
|
|
|
import * as path from 'node:path';
|
|
|
import {
|
|
|
+ createTuiProjectKey,
|
|
|
+ isTuiConfigInvalid,
|
|
|
readTuiSnapshot,
|
|
|
recordTuiAgentModel,
|
|
|
recordTuiAgentModels,
|
|
|
@@ -94,3 +96,108 @@ describe('tui-state persistence', () => {
|
|
|
expect(snapshot.configInvalid).toBe(false);
|
|
|
});
|
|
|
});
|
|
|
+
|
|
|
+describe('tui-state project-scoped configInvalid', () => {
|
|
|
+ test('recordTuiConfigStatus with projectKey sets configInvalidByProject', () => {
|
|
|
+ const projectKey = createTuiProjectKey('/tmp/project-a');
|
|
|
+ recordTuiConfigStatus({ invalid: true, projectKey });
|
|
|
+
|
|
|
+ const snapshot = readTuiSnapshot();
|
|
|
+ expect(snapshot.configInvalidByProject[projectKey]).toBe(true);
|
|
|
+ expect(snapshot.configInvalid).toBe(false); // legacy unchanged
|
|
|
+ });
|
|
|
+
|
|
|
+ test('recordTuiConfigStatus with projectKey false clears that key', () => {
|
|
|
+ const projectKey = createTuiProjectKey('/tmp/project-b');
|
|
|
+ recordTuiConfigStatus({ invalid: true, projectKey });
|
|
|
+ recordTuiConfigStatus({ invalid: false, projectKey });
|
|
|
+
|
|
|
+ const snapshot = readTuiSnapshot();
|
|
|
+ expect(snapshot.configInvalidByProject[projectKey]).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('different project keys do not overwrite each other', () => {
|
|
|
+ const keyA = createTuiProjectKey('/tmp/project-c');
|
|
|
+ const keyB = createTuiProjectKey('/tmp/project-d');
|
|
|
+ recordTuiConfigStatus({ invalid: true, projectKey: keyA });
|
|
|
+ recordTuiConfigStatus({ invalid: false, projectKey: keyB });
|
|
|
+
|
|
|
+ const snapshot = readTuiSnapshot();
|
|
|
+ expect(snapshot.configInvalidByProject[keyA]).toBe(true);
|
|
|
+ expect(snapshot.configInvalidByProject[keyB]).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('isTuiConfigInvalid uses configInvalidByProject when projectKey given', () => {
|
|
|
+ const projectKey = createTuiProjectKey('/tmp/project-e');
|
|
|
+ recordTuiConfigStatus({ invalid: true, projectKey });
|
|
|
+
|
|
|
+ const snapshot = readTuiSnapshot();
|
|
|
+ expect(isTuiConfigInvalid(snapshot, projectKey)).toBe(true);
|
|
|
+ expect(
|
|
|
+ isTuiConfigInvalid(snapshot, createTuiProjectKey('/tmp/other')),
|
|
|
+ ).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('isTuiConfigInvalid falls back to legacy configInvalid when no projectKey', () => {
|
|
|
+ recordTuiConfigStatus({ invalid: true }); // no projectKey = legacy
|
|
|
+ const snapshot = readTuiSnapshot();
|
|
|
+ expect(isTuiConfigInvalid(snapshot)).toBe(true);
|
|
|
+ expect(isTuiConfigInvalid(snapshot, undefined)).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('old snapshot without configInvalidByProject defaults to empty object', () => {
|
|
|
+ const filePath = path.join(
|
|
|
+ tempDir,
|
|
|
+ 'opencode',
|
|
|
+ 'storage',
|
|
|
+ 'oh-my-opencode-slim',
|
|
|
+ 'tui-state.json',
|
|
|
+ );
|
|
|
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
|
+ fs.writeFileSync(
|
|
|
+ filePath,
|
|
|
+ JSON.stringify({ version: 1, updatedAt: Date.now(), agentModels: {} }),
|
|
|
+ );
|
|
|
+
|
|
|
+ const snapshot = readTuiSnapshot();
|
|
|
+ expect(snapshot.configInvalidByProject).toEqual({});
|
|
|
+ });
|
|
|
+
|
|
|
+ test('malformed configInvalidByProject entries are ignored', () => {
|
|
|
+ const projectKey = createTuiProjectKey('/tmp/project-f');
|
|
|
+ const filePath = path.join(
|
|
|
+ tempDir,
|
|
|
+ 'opencode',
|
|
|
+ 'storage',
|
|
|
+ 'oh-my-opencode-slim',
|
|
|
+ 'tui-state.json',
|
|
|
+ );
|
|
|
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
|
+ fs.writeFileSync(
|
|
|
+ filePath,
|
|
|
+ JSON.stringify({
|
|
|
+ version: 1,
|
|
|
+ updatedAt: Date.now(),
|
|
|
+ agentModels: {},
|
|
|
+ configInvalidByProject: { [projectKey]: true, invalid: 'yes' },
|
|
|
+ }),
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(readTuiSnapshot().configInvalidByProject).toEqual({
|
|
|
+ [projectKey]: true,
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ test('createTuiProjectKey produces consistent hash for same path', () => {
|
|
|
+ const key1 = createTuiProjectKey('/tmp/same');
|
|
|
+ const key2 = createTuiProjectKey('/tmp/same');
|
|
|
+ expect(key1).toBe(key2);
|
|
|
+ expect(key1).toHaveLength(16);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('createTuiProjectKey produces different hash for different paths', () => {
|
|
|
+ const key1 = createTuiProjectKey('/tmp/diff1');
|
|
|
+ const key2 = createTuiProjectKey('/tmp/diff2');
|
|
|
+ expect(key1).not.toBe(key2);
|
|
|
+ });
|
|
|
+});
|