Ignore dormant apps: per-app and per-store 'ignored' in the app list; apps ignore/unignore, watch, ws and apps list skip them; MCP tools
This commit is contained in:
parent
75bf62e125
commit
13b91dd423
7 changed files with 278 additions and 30 deletions
104
tests/registry-ignore.test.ts
Normal file
104
tests/registry-ignore.test.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// Dormant apps: `ignored` per entry, whole app or one store side (cli/lib/registry.ts); storebound-side only.
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
activeProjects, activeStoreApps, findEntry, ignoredStoreIds, ignoredStores, isIgnored, parseRegistry, readRegistry, setIgnore, withIgnore, writeRegistry,
|
||||
} from '../cli/lib/registry';
|
||||
import { TOOLS, toArgv } from '../cli/mcp-tools';
|
||||
|
||||
const reg = () => parseRegistry(JSON.stringify({
|
||||
version: 1,
|
||||
projects: [{ id: 'aaaaaaaaaaaaaaaa', path: '/work/star-dash', name: 'Star Dash', added: '' }, { id: 'bbbbbbbbbbbbbbbb', path: '/work/puzzle', name: 'Puzzle', added: '' }],
|
||||
storeApps: [{ store: 'apple', appId: '6400000000', account: 'teamA', name: 'Demo App', bundleId: 'com.example.demo' }, { store: 'play', appId: 'com.example.demo', name: 'Demo Play' }],
|
||||
}));
|
||||
|
||||
describe('registry ignore', () => {
|
||||
test('whole app, one store side, restore; only valid sides are kept', () => {
|
||||
let r = reg();
|
||||
r = withIgnore(r, 'Star Dash', 'apple', true).registry;
|
||||
expect(ignoredStores(r.projects[0]!)).toEqual(['apple']);
|
||||
expect(isIgnored(r.projects[0]!)).toBe(false);
|
||||
expect(isIgnored(r.projects[0]!, 'apple')).toBe(true);
|
||||
expect(isIgnored(r.projects[0]!, 'play')).toBe(false);
|
||||
r = withIgnore(r, 'star dash', undefined, true).registry;
|
||||
expect(isIgnored(r.projects[0]!)).toBe(true);
|
||||
expect(activeProjects(r).map(p => p.name)).toEqual(['Puzzle']);
|
||||
expect(activeProjects(r, 'play').map(p => p.name)).toEqual(['Puzzle']);
|
||||
r = withIgnore(r, 'aaaaaaaaaaaaaaaa', 'play', false).registry;
|
||||
expect(ignoredStores(r.projects[0]!)).toEqual(['apple']);
|
||||
expect(activeProjects(r, 'play').map(p => p.name)).toEqual(['Star Dash', 'Puzzle']);
|
||||
r = withIgnore(r, 'Star Dash', undefined, false).registry;
|
||||
expect(r.projects[0]!.ignored).toBeUndefined();
|
||||
expect(parseRegistry(JSON.stringify({ projects: [{ path: '/a', ignored: ['play', 'x', 'play', 'apple'] }] })).projects[0]!.ignored).toEqual(['apple', 'play']);
|
||||
});
|
||||
|
||||
test('a store-only app has one side: its own store; the other store is an error', () => {
|
||||
const r = reg();
|
||||
expect(() => withIgnore(r, 'apple:6400000000', 'play', true)).toThrow('apple app only');
|
||||
const o = withIgnore(r, 'apple:6400000000', 'apple', true);
|
||||
expect(o.changed).toBe(true);
|
||||
expect(activeStoreApps(o.registry).map(a => a.store)).toEqual(['play']);
|
||||
expect(withIgnore(o.registry, 'apple:6400000000', undefined, true).changed).toBe(false);
|
||||
});
|
||||
|
||||
test('ambiguous names and unknown keys fail loudly', () => {
|
||||
const r = reg();
|
||||
expect(() => withIgnore(r, 'nope', undefined, true)).toThrow('not in the app list');
|
||||
expect(findEntry(r, 'Demo App')?.kind).toBe('storeApp');
|
||||
const dup = parseRegistry(JSON.stringify({ projects: [{ path: '/a/x', name: 'Same' }, { path: '/b/x', name: 'Same' }] }));
|
||||
expect(() => findEntry(dup, 'Same')).toThrow('matches 2 apps');
|
||||
});
|
||||
|
||||
test('written to the file and read back; unrelated entries and storeApps stay', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'sb-ign-'));
|
||||
const file = join(dir, 'projects.json');
|
||||
writeRegistry(reg(), file);
|
||||
expect(setIgnore('Puzzle', 'play', true, file).changed).toBe(true);
|
||||
expect(setIgnore('Puzzle', 'play', true, file).changed).toBe(false);
|
||||
setIgnore('play:com.example.demo', undefined, true, file);
|
||||
const back = readRegistry(file);
|
||||
expect(back.projects.find(p => p.name === 'Puzzle')!.ignored).toEqual(['play']);
|
||||
expect(back.storeApps!.find(a => a.store === 'play')!.ignored).toEqual(['play']);
|
||||
expect(back.storeApps!.find(a => a.store === 'apple')!.ignored).toBeUndefined();
|
||||
expect(JSON.parse(readFileSync(file, 'utf8')).projects[0].ignored).toBeUndefined();
|
||||
});
|
||||
|
||||
test('store ids of ignored sides come from the config text, never by running it', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'sb-ign-'));
|
||||
const app = join(dir, 'game'); mkdirSync(app);
|
||||
writeFileSync(join(app, 'storebound.config.ts'), "export default { apple: { bundleId: 'com.example.game', appId: '6400000123' }, play: { packageName: 'com.example.game.android' } };\n");
|
||||
let r = parseRegistry(JSON.stringify({ projects: [{ path: app, name: 'Game' }] }));
|
||||
r = withIgnore(r, 'Game', 'play', true).registry;
|
||||
const ids = ignoredStoreIds(r);
|
||||
expect([...ids.play]).toEqual(['com.example.game.android']);
|
||||
expect(ids.apple.size).toBe(0);
|
||||
});
|
||||
|
||||
test('MCP tools: ignore and unignore build the argv, writes default to dry run', () => {
|
||||
const t = TOOLS.find(x => x.name === 'apps_ignore')!;
|
||||
expect(t.write).toBe(true);
|
||||
expect(toArgv(t, { app: 'Star Dash', store: 'apple' }).argv).toEqual(['apps', 'ignore', 'Star Dash', '--store', 'apple', '--dry']);
|
||||
expect(TOOLS.some(x => x.name === 'apps_unignore')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('watch and ws skip ignored apps', () => {
|
||||
test('watch targets: a fully ignored app is not watched, a half-ignored one skips that store', async () => {
|
||||
const { listTargets } = await import('../cli/watch');
|
||||
const { parseArgs } = await import('../cli/lib/args');
|
||||
const base = mkdtempSync(join(tmpdir(), 'sb-ign-watch-'));
|
||||
const mk = (n: string) => { const d = join(base, n); mkdirSync(d); writeFileSync(join(d, 'storebound.config.ts'), 'export default {};\n'); return d; };
|
||||
const file = join(base, 'projects.json');
|
||||
writeRegistry(parseRegistry(JSON.stringify({ projects: [{ path: mk('a'), name: 'A' }, { path: mk('b'), name: 'B' }, { path: mk('c'), name: 'C' }] })), file);
|
||||
setIgnore('B', undefined, true, file);
|
||||
setIgnore('C', 'play', true, file);
|
||||
const saved = process.env.STOREBOUND_REGISTRY;
|
||||
process.env.STOREBOUND_REGISTRY = file;
|
||||
try {
|
||||
const t = await listTargets(parseArgs([]));
|
||||
expect(t.map(x => [x.name, x.skip])).toEqual([['A', []], ['C', ['play']]]);
|
||||
} finally { if (saved === undefined) delete process.env.STOREBOUND_REGISTRY; else process.env.STOREBOUND_REGISTRY = saved; }
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue