Skip to content

Commit 56f585f

Browse files
authored
Merge pull request #26 from TrueNine/dev
Dev
2 parents 23a8066 + f2e9976 commit 56f585f

File tree

13 files changed

+684
-16
lines changed

13 files changed

+684
-16
lines changed

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@truenine/memory-sync-cli",
33
"type": "module",
4-
"version": "2026.10219.10518",
4+
"version": "2026.10219.11823",
55
"description": "TrueNine Memory Synchronization CLI",
66
"author": "TrueNine",
77
"license": "AGPL-3.0-only",

cli/public/tnmsc.example.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "2026.10219.10518",
2+
"version": "2026.10219.11823",
33
"workspaceDir": "~/project",
44
"shadowSourceProject": {
55
"name": "tnmsc-shadow",
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import type {OutputPluginContext} from '@/types'
2+
import * as fs from 'node:fs'
3+
import * as os from 'node:os'
4+
import * as path from 'node:path'
5+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
6+
import {collectFileNames, createMockProject, createMockRulePrompt} from './__testUtils__'
7+
import {OpencodeCLIOutputPlugin} from './OpencodeCLIOutputPlugin'
8+
9+
class TestableOpencodeCLIOutputPlugin extends OpencodeCLIOutputPlugin {
10+
private mockHomeDir: string | null = null
11+
12+
public setMockHomeDir(dir: string | null): void {
13+
this.mockHomeDir = dir
14+
}
15+
16+
protected override getHomeDir(): string {
17+
if (this.mockHomeDir != null) return this.mockHomeDir
18+
return super.getHomeDir()
19+
}
20+
}
21+
22+
function createMockContext(
23+
tempDir: string,
24+
rules: unknown[],
25+
projects: unknown[]
26+
): OutputPluginContext {
27+
return {
28+
collectedInputContext: {
29+
workspace: {
30+
projects: projects as never,
31+
directory: {
32+
pathKind: 1,
33+
path: tempDir,
34+
basePath: tempDir,
35+
getDirectoryName: () => 'workspace',
36+
getAbsolutePath: () => tempDir
37+
}
38+
},
39+
ideConfigFiles: [],
40+
rules: rules as never,
41+
fastCommands: [],
42+
skills: [],
43+
globalMemory: void 0,
44+
aiAgentIgnoreConfigFiles: [],
45+
subAgents: []
46+
},
47+
logger: {
48+
debug: vi.fn(),
49+
trace: vi.fn(),
50+
info: vi.fn(),
51+
warn: vi.fn(),
52+
error: vi.fn()
53+
} as never,
54+
fs,
55+
path,
56+
glob: vi.fn() as never
57+
}
58+
}
59+
60+
describe('opencodeCLIOutputPlugin - projectConfig filtering', () => {
61+
let tempDir: string,
62+
plugin: TestableOpencodeCLIOutputPlugin
63+
64+
beforeEach(() => {
65+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-proj-config-test-'))
66+
plugin = new TestableOpencodeCLIOutputPlugin()
67+
plugin.setMockHomeDir(tempDir)
68+
})
69+
70+
afterEach(() => {
71+
try {
72+
fs.rmSync(tempDir, {recursive: true, force: true})
73+
}
74+
catch {}
75+
})
76+
77+
describe('registerProjectOutputFiles', () => {
78+
it('should include all project rules when no projectConfig', async () => {
79+
const rules = [
80+
createMockRulePrompt('test', 'rule1', 'uniapp', 'project'),
81+
createMockRulePrompt('test', 'rule2', 'vue', 'project')
82+
]
83+
const projects = [createMockProject('proj1', tempDir, 'proj1')]
84+
const ctx = createMockContext(tempDir, rules, projects)
85+
86+
const results = await plugin.registerProjectOutputFiles(ctx)
87+
const fileNames = collectFileNames(results)
88+
89+
expect(fileNames).toContain('rule-test-rule1.md')
90+
expect(fileNames).toContain('rule-test-rule2.md')
91+
})
92+
93+
it('should filter rules by include in projectConfig', async () => {
94+
const rules = [
95+
createMockRulePrompt('test', 'rule1', 'uniapp', 'project'),
96+
createMockRulePrompt('test', 'rule2', 'vue', 'project')
97+
]
98+
const projects = [
99+
createMockProject('proj1', tempDir, 'proj1', {rules: {include: ['uniapp']}})
100+
]
101+
const ctx = createMockContext(tempDir, rules, projects)
102+
103+
const results = await plugin.registerProjectOutputFiles(ctx)
104+
const fileNames = collectFileNames(results)
105+
106+
expect(fileNames).toContain('rule-test-rule1.md')
107+
expect(fileNames).not.toContain('rule-test-rule2.md')
108+
})
109+
110+
it('should filter rules by exclude in projectConfig', async () => {
111+
const rules = [
112+
createMockRulePrompt('test', 'rule1', 'uniapp', 'project'),
113+
createMockRulePrompt('test', 'rule2', 'vue', 'project')
114+
]
115+
const projects = [
116+
createMockProject('proj1', tempDir, 'proj1', {rules: {exclude: ['uniapp']}})
117+
]
118+
const ctx = createMockContext(tempDir, rules, projects)
119+
120+
const results = await plugin.registerProjectOutputFiles(ctx)
121+
const fileNames = collectFileNames(results)
122+
123+
expect(fileNames).not.toContain('rule-test-rule1.md')
124+
expect(fileNames).toContain('rule-test-rule2.md')
125+
})
126+
127+
it('should include rules without seriName regardless of include filter', async () => {
128+
const rules = [
129+
createMockRulePrompt('test', 'rule1', void 0, 'project'),
130+
createMockRulePrompt('test', 'rule2', 'vue', 'project')
131+
]
132+
const projects = [
133+
createMockProject('proj1', tempDir, 'proj1', {rules: {include: ['uniapp']}})
134+
]
135+
const ctx = createMockContext(tempDir, rules, projects)
136+
137+
const results = await plugin.registerProjectOutputFiles(ctx)
138+
const fileNames = collectFileNames(results)
139+
140+
expect(fileNames).toContain('rule-test-rule1.md')
141+
expect(fileNames).not.toContain('rule-test-rule2.md')
142+
})
143+
144+
it('should filter independently for each project', async () => {
145+
const rules = [
146+
createMockRulePrompt('test', 'rule1', 'uniapp', 'project'),
147+
createMockRulePrompt('test', 'rule2', 'vue', 'project')
148+
]
149+
const projects = [
150+
createMockProject('proj1', tempDir, 'proj1', {rules: {include: ['uniapp']}}),
151+
createMockProject('proj2', tempDir, 'proj2', {rules: {include: ['vue']}})
152+
]
153+
const ctx = createMockContext(tempDir, rules, projects)
154+
155+
const results = await plugin.registerProjectOutputFiles(ctx)
156+
const fileNames = results.map(r => ({
157+
path: r.path,
158+
fileName: r.path.split(/[/\\]/).pop()
159+
}))
160+
161+
expect(fileNames.some(f => f.path.includes('proj1') && f.fileName === 'rule-test-rule1.md')).toBe(true)
162+
expect(fileNames.some(f => f.path.includes('proj1') && f.fileName === 'rule-test-rule2.md')).toBe(false)
163+
expect(fileNames.some(f => f.path.includes('proj2') && f.fileName === 'rule-test-rule2.md')).toBe(true)
164+
expect(fileNames.some(f => f.path.includes('proj2') && f.fileName === 'rule-test-rule1.md')).toBe(false)
165+
})
166+
167+
it('should return empty when include matches nothing', async () => {
168+
const rules = [
169+
createMockRulePrompt('test', 'rule1', 'uniapp', 'project')
170+
]
171+
const projects = [
172+
createMockProject('proj1', tempDir, 'proj1', {rules: {include: ['react']}})
173+
]
174+
const ctx = createMockContext(tempDir, rules, projects)
175+
176+
const results = await plugin.registerProjectOutputFiles(ctx)
177+
const ruleFiles = results.filter(r => r.path.includes('rule-'))
178+
179+
expect(ruleFiles).toHaveLength(0)
180+
})
181+
})
182+
183+
describe('registerProjectOutputDirs', () => {
184+
it('should not register rules dir when all rules filtered out', async () => {
185+
const rules = [
186+
createMockRulePrompt('test', 'rule1', 'uniapp', 'project')
187+
]
188+
const projects = [
189+
createMockProject('proj1', tempDir, 'proj1', {rules: {include: ['react']}})
190+
]
191+
const ctx = createMockContext(tempDir, rules, projects)
192+
193+
const results = await plugin.registerProjectOutputDirs(ctx)
194+
const rulesDirs = results.filter(r => r.path.includes('rules'))
195+
196+
expect(rulesDirs).toHaveLength(0)
197+
})
198+
199+
it('should register rules dir when rules match filter', async () => {
200+
const rules = [
201+
createMockRulePrompt('test', 'rule1', 'uniapp', 'project')
202+
]
203+
const projects = [
204+
createMockProject('proj1', tempDir, 'proj1', {rules: {include: ['uniapp']}})
205+
]
206+
const ctx = createMockContext(tempDir, rules, projects)
207+
208+
const results = await plugin.registerProjectOutputDirs(ctx)
209+
const rulesDirs = results.filter(r => r.path.includes('rules'))
210+
211+
expect(rulesDirs.length).toBeGreaterThan(0)
212+
})
213+
})
214+
215+
describe('project rules directory path', () => {
216+
it('should use .opencode/rules/ for project rules', async () => {
217+
const rules = [
218+
createMockRulePrompt('test', 'rule1', 'uniapp', 'project')
219+
]
220+
const projects = [createMockProject('proj1', tempDir, 'proj1')]
221+
const ctx = createMockContext(tempDir, rules, projects)
222+
223+
const results = await plugin.registerProjectOutputFiles(ctx)
224+
const ruleFile = results.find(r => r.path.includes('rule-test-rule1.md'))
225+
226+
expect(ruleFile).toBeDefined()
227+
expect(ruleFile?.path).toContain('.opencode')
228+
expect(ruleFile?.path).toContain('rules')
229+
})
230+
})
231+
})

0 commit comments

Comments
 (0)