Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
54f984d
fix(tests): eliminate flaky concurrent-writes test by awaiting write …
frap129 Apr 25, 2026
93d06e3
refactor: remove pass-through wrappers createSessionStore and extract…
frap129 Apr 25, 2026
9e3be25
refactor: extract shared logWarning helper and remove redundant guard
frap129 Apr 25, 2026
f004d2f
style: remove restating comments in rule-discovery, rule-metadata, an…
frap129 Apr 25, 2026
80fd6bf
chore: ignore .desloppify directory and scorecard.png
frap129 Apr 25, 2026
bfbc145
refactor(runtime-context): move debugLog and projectDirectory into Bu…
frap129 Apr 25, 2026
6f1f6e3
refactor(active-rules-state): remove fire-and-forget catch so callers…
frap129 Apr 25, 2026
bdcccfd
refactor: standardize I/O boundary error returns to null instead of u…
frap129 Apr 25, 2026
58ba71c
refactor(runtime): replace as any cast with typed OpenCodeClient inte…
frap129 Apr 25, 2026
f3f0119
refactor(runtime): add completion audit log for hook side-effects
frap129 Apr 25, 2026
728a1ad
fix(message-context): strictly require role === 'user' in extractLate…
frap129 Apr 25, 2026
0af29be
fix(active-rules-state): throw on invalid sessionId to align with get…
frap129 Apr 25, 2026
76fbeef
style(test-fixtures): rename duplicate section headers to be unique
frap129 Apr 25, 2026
8c8f7e0
style: add .js extension to local imports in active-rules-state and r…
frap129 Apr 25, 2026
fa6b160
style: add node: prefix to all Node built-in module imports
frap129 Apr 25, 2026
34262df
style(active-rules-state): rename sessionId to sessionID to match con…
frap129 Apr 25, 2026
c61448d
style(tui): add .js extension to cross-package imports from src
frap129 Apr 25, 2026
c3f58c0
docs(utils): clarify barrel file as intentional public API facade
frap129 Apr 25, 2026
ee0ffec
refactor: extract shared hasConditions to rule-metadata and deduplica…
frap129 Apr 25, 2026
88ecc75
docs(utils): explicitly document internal modules not re-exported by …
frap129 Apr 25, 2026
1be1050
refactor: extend utils.ts facade for TUI needs and update cross-packa…
frap129 Apr 25, 2026
f8c67c4
fix(deps): remove duplicated @opentui/* and solid-js from devDependen…
frap129 Apr 25, 2026
b11642e
fix(deps): add @opentui/* as optional peerDependencies
frap129 Apr 25, 2026
7f09415
fix(deps): remove redundant @types/minimatch
frap129 Apr 25, 2026
797fb1e
fix(deps): align @opentui versions in manifest with lock file (0.1.93…
frap129 Apr 25, 2026
ba14f11
refactor(rule-filter): extract repeated condition checks into evaluat…
frap129 Apr 25, 2026
387f1f9
refactor(tui): consolidate loadRules and deduplicate metadata display…
frap129 Apr 25, 2026
11b23f4
fix: improve error consistency, fix cross-module imports, and update …
frap129 Apr 25, 2026
baa2d92
refactor: fix convention drift, naming, and AI debt across 10 files
frap129 Apr 25, 2026
d5bee5c
refactor(error-consistency): add await to write call, fix type duplic…
frap129 Apr 26, 2026
d22c78c
fix(bun-compat): achieve 100% pass rate by eliminating vi.mock and ad…
frap129 May 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,5 @@ temp/
.opencode/plans

# Desloppify runtime artifacts
.desloppify/external_review_sessions/
.desloppify/review_packets/
.desloppify/subagents/
.desloppify/*.bak
.desloppify/review_packet_blind.json
.desloppify/
scorecard.png
20 changes: 13 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,26 +67,32 @@
},
"peerDependencies": {
"@opencode-ai/plugin": "^1.3.7",
"@opencode-ai/sdk": "^1.3.7"
"@opencode-ai/sdk": "^1.3.7",
"@opentui/core": "^0.1.97",
"@opentui/solid": "^0.1.97"
},
"peerDependenciesMeta": {
"@opentui/core": {
"optional": true
},
"@opentui/solid": {
"optional": true
}
},
"devDependencies": {
"@opencode-ai/plugin": "^1.3.9",
"@opencode-ai/sdk": "^1.3.9",
"@opentui/core": "^0.1.93",
"@opentui/solid": "^0.1.93",
"@types/minimatch": "^5.1.2",
"@types/node": "^20.19.30",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.57.1",
"prettier": "^3.8.1",
"solid-js": "1.9.11",
"typescript": "^5.9.3",
"vitest": "^1.6.1"
},
"dependencies": {
"@opentui/core": "^0.1.93",
"@opentui/solid": "^0.1.93",
"@opentui/core": "^0.1.97",
"@opentui/solid": "^0.1.97",
"minimatch": "^9.0.5",
"solid-js": "1.9.11",
"yaml": "^2.8.2"
Expand Down
122 changes: 45 additions & 77 deletions src/active-rules-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
writeActiveRulesState,
readActiveRulesState,
_setStateDirForTesting,
_resetWriteQueues,
} from './active-rules-state.js';

describe('active-rules-state', () => {
Expand All @@ -27,6 +28,7 @@ describe('active-rules-state', () => {
afterEach(async () => {
// Reset the override
_setStateDirForTesting(null);
_resetWriteQueues();

// Clean up test directory
if (testStateDir) {
Expand Down Expand Up @@ -61,33 +63,30 @@ describe('active-rules-state', () => {
expect(filePath).toBe(path.join(testStateDir, 'ses_123.json'));
});

it('throws for sessionId with path traversal', () => {
expect(() => getStateFilePath('../escape')).toThrow('Invalid sessionId');
expect(() => getStateFilePath('foo/bar')).toThrow('Invalid sessionId');
expect(() => getStateFilePath('/absolute')).toThrow('Invalid sessionId');
it('throws for sessionID with path traversal', () => {
expect(() => getStateFilePath('../escape')).toThrow('Invalid sessionID');
expect(() => getStateFilePath('foo/bar')).toThrow('Invalid sessionID');
expect(() => getStateFilePath('/absolute')).toThrow('Invalid sessionID');
});

it('throws for sessionId with special characters', () => {
expect(() => getStateFilePath('ses.123')).toThrow('Invalid sessionId');
expect(() => getStateFilePath('ses 123')).toThrow('Invalid sessionId');
expect(() => getStateFilePath('')).toThrow('Invalid sessionId');
it('throws for sessionID with special characters', () => {
expect(() => getStateFilePath('ses.123')).toThrow('Invalid sessionID');
expect(() => getStateFilePath('ses 123')).toThrow('Invalid sessionID');
expect(() => getStateFilePath('')).toThrow('Invalid sessionID');
});
});

describe('writeActiveRulesState and readActiveRulesState', () => {
it('write/read round-trip preserves data', async () => {
const sessionId = 'ses_roundtrip';
const sessionID = 'ses_roundtrip';
const matchedPaths = ['/path/to/rule1.md', '/path/to/rule2.md'];

writeActiveRulesState(sessionId, matchedPaths);
await writeActiveRulesState(sessionID, matchedPaths);

// Wait for the fire-and-forget write to complete
await waitForFile(getStateFilePath(sessionId));

const state = await readActiveRulesState(sessionId);
const state = await readActiveRulesState(sessionID);

expect(state).not.toBeNull();
expect(state!.sessionId).toBe(sessionId);
expect(state!.sessionID).toBe(sessionID);
expect(state!.matchedRulePaths).toEqual(matchedPaths);
expect(typeof state!.evaluatedAt).toBe('number');
expect(state!.evaluatedAt).toBeLessThanOrEqual(Date.now());
Expand Down Expand Up @@ -125,7 +124,7 @@ describe('active-rules-state', () => {
await fs.writeFile(
filePath,
JSON.stringify({
sessionId: 123,
sessionID: 123,
matchedRulePaths: 'not-an-array',
evaluatedAt: 'not-a-number',
}),
Expand All @@ -143,7 +142,7 @@ describe('active-rules-state', () => {
await fs.writeFile(
filePath,
JSON.stringify({
sessionId: 'ses_badarray',
sessionID: 'ses_badarray',
matchedRulePaths: ['/valid.md', 123, null],
evaluatedAt: Date.now(),
}),
Expand All @@ -154,36 +153,26 @@ describe('active-rules-state', () => {
expect(state).toBeNull();
});

it('silently ignores write with invalid sessionId', async () => {
writeActiveRulesState('../escape', ['/rule.md']);
writeActiveRulesState('foo/bar', ['/rule.md']);

// Give time for any writes to occur
await new Promise(resolve => setTimeout(resolve, 50));

// Verify no files were created
try {
await fs.access(testStateDir);
const files = await fs.readdir(testStateDir);
expect(files).toHaveLength(0);
} catch {
// Directory doesn't exist, which is expected
}
it('throws on write with invalid sessionID', () => {
expect(() => writeActiveRulesState('../escape', ['/rule.md'])).toThrow(
'Invalid sessionID'
);
expect(() => writeActiveRulesState('foo/bar', ['/rule.md'])).toThrow(
'Invalid sessionID'
);
});

it('returns null for read with invalid sessionId', async () => {
const state = await readActiveRulesState('../escape');
expect(state).toBeNull();
it('throws for read with invalid sessionID', async () => {
await expect(readActiveRulesState('../escape')).rejects.toThrow(
'Invalid sessionID'
);
});

it('no temp file remains after write', async () => {
const sessionId = 'ses_no_temp';
const sessionID = 'ses_no_temp';
const matchedPaths = ['/rule.md'];

writeActiveRulesState(sessionId, matchedPaths);

// Wait for write to complete
await waitForFile(getStateFilePath(sessionId));
await writeActiveRulesState(sessionID, matchedPaths);

// Check that no temp files remain
const files = await fs.readdir(testStateDir);
Expand All @@ -193,49 +182,42 @@ describe('active-rules-state', () => {
});

it('serializes concurrent writes for same session', async () => {
const sessionId = 'ses_concurrent';
const sessionID = 'ses_concurrent';

// Fire multiple writes concurrently
writeActiveRulesState(sessionId, ['path1']);
writeActiveRulesState(sessionId, ['path2']);
writeActiveRulesState(sessionId, ['path3']);
const first = writeActiveRulesState(sessionID, ['path1']);
const second = writeActiveRulesState(sessionID, ['path2']);
const third = writeActiveRulesState(sessionID, ['path3']);

// Wait for all writes to complete
await waitForFile(getStateFilePath(sessionId));

// Give a bit more time for all queued writes to finish
await new Promise(resolve => setTimeout(resolve, 100));
await Promise.all([first, second, third]);

// The final state should reflect the last write
const state = await readActiveRulesState(sessionId);
const state = await readActiveRulesState(sessionID);
expect(state).not.toBeNull();
expect(state!.matchedRulePaths).toEqual(['path3']);
});

it('creates state directory when it does not exist', async () => {
const sessionId = 'ses_newdir';
const sessionID = 'ses_newdir';
const matchedPaths = ['/rule.md'];

// Verify directory doesn't exist yet
await expect(fs.access(testStateDir)).rejects.toThrow();

writeActiveRulesState(sessionId, matchedPaths);

// Wait for write to complete
await waitForFile(getStateFilePath(sessionId));
await writeActiveRulesState(sessionID, matchedPaths);

// Verify directory now exists
await expect(fs.access(testStateDir)).resolves.toBeUndefined();
// Verify directory now exists — fs.access resolves to null on Bun, undefined on Node
const dirExists = await fs.access(testStateDir).then(
() => true,
() => false
);
expect(dirExists).toBe(true);
});

it('handles writes to different sessions independently', async () => {
writeActiveRulesState('ses_a', ['ruleA']);
writeActiveRulesState('ses_b', ['ruleB']);

// Wait for both writes
await Promise.all([
waitForFile(getStateFilePath('ses_a')),
waitForFile(getStateFilePath('ses_b')),
writeActiveRulesState('ses_a', ['ruleA']),
writeActiveRulesState('ses_b', ['ruleB']),
]);

const stateA = await readActiveRulesState('ses_a');
Expand All @@ -246,17 +228,3 @@ describe('active-rules-state', () => {
});
});
});

// Helper to wait for a file to exist
async function waitForFile(filePath: string, timeoutMs = 1000): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
await fs.access(filePath);
return;
} catch {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
throw new Error(`Timed out waiting for file: ${filePath}`);
}
Loading
Loading