tribunal-kit 4.4.1 → 4.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/history/architecture-graph.yaml +140 -0
- package/.agent/history/graph-cache.json +262 -0
- package/.agent/history/snapshots/bin__tribunal-kit.js.json +19 -0
- package/.agent/history/snapshots/eslint.config.js.json +9 -0
- package/.agent/history/snapshots/migrate_refs.js.json +11 -0
- package/.agent/history/snapshots/scripts__changelog.js.json +13 -0
- package/.agent/history/snapshots/scripts__sync-version.js.json +12 -0
- package/.agent/history/snapshots/scripts__validate-payload.js.json +12 -0
- package/.agent/history/snapshots/test__integration__bridges.test.js.json +14 -0
- package/.agent/history/snapshots/test__integration__init.test.js.json +14 -0
- package/.agent/history/snapshots/test__integration__routing.test.js.json +12 -0
- package/.agent/history/snapshots/test__integration__swarm_dispatcher.test.js.json +14 -0
- package/.agent/history/snapshots/test__integration__wave2.test.js.json +14 -0
- package/.agent/history/snapshots/test__unit__args.test.js.json +20 -0
- package/.agent/history/snapshots/test__unit__case_law_manager.test.js.json +11 -0
- package/.agent/history/snapshots/test__unit__context_broker.test.js.json +11 -0
- package/.agent/history/snapshots/test__unit__copyDir.test.js.json +23 -0
- package/.agent/history/snapshots/test__unit__graph_tools.test.js.json +12 -0
- package/.agent/history/snapshots/test__unit__inner_loop_validator.test.js.json +11 -0
- package/.agent/history/snapshots/test__unit__selfInstall.test.js.json +23 -0
- package/.agent/history/snapshots/test__unit__semver.test.js.json +20 -0
- package/.agent/history/snapshots/test__unit__swarm_dispatcher.test.js.json +12 -0
- package/.agent/scripts/_colors.js +170 -18
- package/.agent/scripts/_utils.js +244 -42
- package/.agent/scripts/bundle_analyzer.js +261 -290
- package/.agent/scripts/case_law_manager.js +1 -7
- package/.agent/scripts/checklist.js +278 -266
- package/.agent/scripts/colors.js +11 -17
- package/.agent/scripts/context_broker.js +1 -7
- package/.agent/scripts/dependency_analyzer.js +234 -272
- package/.agent/scripts/graph_builder.js +46 -18
- package/.agent/scripts/graph_visualizer.js +10 -4
- package/.agent/scripts/graph_zoom.js +6 -4
- package/.agent/scripts/inner_loop_validator.js +2 -8
- package/.agent/scripts/lint_runner.js +186 -187
- package/.agent/scripts/schema_validator.js +8 -25
- package/.agent/scripts/security_scan.js +276 -303
- package/.agent/scripts/session_manager.js +1 -7
- package/.agent/scripts/skill_evolution.js +1 -8
- package/.agent/scripts/skill_integrator.js +1 -7
- package/.agent/scripts/test_runner.js +186 -193
- package/.agent/scripts/utils.js +17 -32
- package/.agent/scripts/verify_all.js +248 -257
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/integration/bridges.test.js",
|
|
3
|
+
"hash": "9941a025cf46512d813660e4a09bf304f7fce64f",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"child_process": [],
|
|
8
|
+
"path": [],
|
|
9
|
+
"fs": [],
|
|
10
|
+
"os": []
|
|
11
|
+
},
|
|
12
|
+
"dependents": [],
|
|
13
|
+
"content": "'use strict';\r\n\r\nconst { spawnSync } = require('child_process');\r\nconst path = require('path');\r\nconst fs = require('fs');\r\nconst os = require('os');\r\n\r\nconst CLI = path.resolve(__dirname, '../../bin/tribunal-kit.js');\r\n\r\nfunction runCLI(args = [], opts = {}) {\r\n return spawnSync(process.execPath, [CLI, ...args], {\r\n encoding : 'utf8',\r\n timeout : 30000,\r\n env : { ...process.env, TK_SKIP_UPDATE_CHECK: '1' },\r\n ...opts,\r\n });\r\n}\r\n\r\ndescribe('IDE Bridge File Generation', () => {\r\n let tmpDir;\r\n\r\n beforeEach(() => {\r\n tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tk-bridge-'));\r\n });\r\n\r\n afterEach(() => {\r\n fs.rmSync(tmpDir, { recursive: true, force: true });\r\n });\r\n\r\n test('init creates .cursorrules', () => {\r\n runCLI(['init', '--path', tmpDir, '--skip-update-check']);\r\n const cursorRules = path.join(tmpDir, '.cursorrules');\r\n expect(fs.existsSync(cursorRules)).toBe(true);\r\n const content = fs.readFileSync(cursorRules, 'utf8');\r\n expect(content).toContain('Tribunal');\r\n expect(content).toContain('Anti-Hallucination');\r\n });\r\n\r\n test('init creates .windsurfrules', () => {\r\n runCLI(['init', '--path', tmpDir, '--skip-update-check']);\r\n const windsurfRules = path.join(tmpDir, '.windsurfrules');\r\n expect(fs.existsSync(windsurfRules)).toBe(true);\r\n const content = fs.readFileSync(windsurfRules, 'utf8');\r\n expect(content).toContain('Windsurf Bridge');\r\n expect(content).toContain('Tribunal');\r\n });\r\n\r\n test('init creates .gemini/settings.json', () => {\r\n runCLI(['init', '--path', tmpDir, '--skip-update-check']);\r\n const geminiSettings = path.join(tmpDir, '.gemini', 'settings.json');\r\n expect(fs.existsSync(geminiSettings)).toBe(true);\r\n const json = JSON.parse(fs.readFileSync(geminiSettings, 'utf8'));\r\n expect(json.rules).toBeDefined();\r\n expect(json.rules[0].path).toContain('.agent/rules/GEMINI.md');\r\n });\r\n\r\n test('init creates .github/copilot-instructions.md', () => {\r\n runCLI(['init', '--path', tmpDir, '--skip-update-check']);\r\n const copilot = path.join(tmpDir, '.github', 'copilot-instructions.md');\r\n expect(fs.existsSync(copilot)).toBe(true);\r\n const content = fs.readFileSync(copilot, 'utf8');\r\n expect(content).toContain('Copilot Bridge');\r\n });\r\n\r\n test('bridge files are NOT overwritten on re-init', () => {\r\n runCLI(['init', '--path', tmpDir, '--skip-update-check']);\r\n const cursorRules = path.join(tmpDir, '.cursorrules');\r\n fs.writeFileSync(cursorRules, '# My custom rules\\nDo not touch');\r\n runCLI(['init', '--path', tmpDir, '--force', '--skip-update-check']);\r\n const content = fs.readFileSync(cursorRules, 'utf8');\r\n expect(content).toBe('# My custom rules\\nDo not touch');\r\n });\r\n\r\n test('all bridge files contain the routing table', () => {\r\n runCLI(['init', '--path', tmpDir, '--skip-update-check']);\r\n const content = fs.readFileSync(path.join(tmpDir, '.cursorrules'), 'utf8');\r\n expect(content).toContain('backend-specialist');\r\n expect(content).toContain('frontend-specialist');\r\n });\r\n\r\n test('init --dry-run does NOT create bridge files', () => {\r\n runCLI(['init', '--path', tmpDir, '--dry-run', '--skip-update-check']);\r\n expect(fs.existsSync(path.join(tmpDir, '.cursorrules'))).toBe(false);\r\n });\r\n});\r\n"
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/integration/init.test.js",
|
|
3
|
+
"hash": "43387fa138c7bdde2ee4d42d07ec3c82ed754275",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"child_process": [],
|
|
8
|
+
"path": [],
|
|
9
|
+
"fs": [],
|
|
10
|
+
"os": []
|
|
11
|
+
},
|
|
12
|
+
"dependents": [],
|
|
13
|
+
"content": "'use strict';\r\n\r\nconst { spawnSync } = require('child_process');\r\nconst path = require('path');\r\nconst fs = require('fs');\r\nconst os = require('os');\r\n\r\nconst CLI = path.resolve(__dirname, '../../bin/tribunal-kit.js');\r\n\r\nfunction runCLI(args = [], opts = {}) {\r\n return spawnSync(process.execPath, [CLI, ...args], {\r\n encoding : 'utf8',\r\n timeout : 15000,\r\n env : { ...process.env, TK_SKIP_UPDATE_CHECK: '1' },\r\n ...opts,\r\n });\r\n}\r\n\r\ndescribe('tribunal-kit init command', () => {\r\n let tmpDir;\r\n\r\n beforeEach(() => {\r\n tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tk-init-test-'));\r\n });\r\n\r\n afterEach(() => {\r\n fs.rmSync(tmpDir, { recursive: true, force: true });\r\n });\r\n\r\n test('init --path installs .agent/ and exits 0', () => {\r\n const result = runCLI(['init', '--path', tmpDir, '--skip-update-check']);\r\n expect(result.status).toBe(0);\r\n expect(fs.existsSync(path.join(tmpDir, '.agent'))).toBe(true);\r\n });\r\n\r\n test('init --dry-run writes NO files and exits 0', () => {\r\n const dryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tk-dry-'));\r\n fs.rmSync(dryDir, { recursive: true, force: true });\r\n fs.mkdirSync(dryDir);\r\n const result = runCLI(['init', '--path', dryDir, '--dry-run', '--skip-update-check']);\r\n expect(result.status).toBe(0);\r\n expect(fs.existsSync(path.join(dryDir, '.agent'))).toBe(false);\r\n fs.rmSync(dryDir, { recursive: true, force: true });\r\n });\r\n\r\n test('init warns and exits 0 when .agent already exists (no --force)', () => {\r\n // Pre-create the .agent dir\r\n fs.mkdirSync(path.join(tmpDir, '.agent'));\r\n const result = runCLI(['init', '--path', tmpDir, '--skip-update-check']);\r\n expect(result.status).toBe(0);\r\n // Should warn about existing install\r\n expect(result.stdout).toMatch(/already exists|--force/i);\r\n });\r\n\r\n test('init --quiet suppresses main output and exits 0', () => {\r\n const result = runCLI(['init', '--path', tmpDir, '--quiet', '--skip-update-check']);\r\n expect(result.status).toBe(0);\r\n // --quiet suppresses the big banner/card but dim() may still emit\r\n // a version line; what matters is the .agent/ folder was installed\r\n expect(fs.existsSync(path.join(tmpDir, '.agent'))).toBe(true);\r\n });\r\n\r\n test('init --force overwrites existing .agent/ and exits 0', () => {\r\n // First install\r\n runCLI(['init', '--path', tmpDir, '--skip-update-check']);\r\n // Write a marker inside a known-cleaned subdir (agents/)\r\n const marker = path.join(tmpDir, '.agent', 'agents', '__test_marker__.md');\r\n fs.writeFileSync(marker, 'marker');\r\n // Force reinstall\r\n const result = runCLI(['init', '--path', tmpDir, '--force', '--skip-update-check']);\r\n expect(result.status).toBe(0);\r\n // The agents/ subdir is wiped and rebuilt, so the extra marker is gone\r\n expect(fs.existsSync(marker)).toBe(false);\r\n });\r\n});\r\n\r\ndescribe('tribunal-kit status command', () => {\r\n let tmpDir;\r\n\r\n beforeEach(() => {\r\n tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tk-status-test-'));\r\n });\r\n\r\n afterEach(() => {\r\n fs.rmSync(tmpDir, { recursive: true, force: true });\r\n });\r\n\r\n test('status exits 0 when not installed', () => {\r\n const result = runCLI(['status', '--path', tmpDir]);\r\n expect(result.status).toBe(0);\r\n expect(result.stdout).toMatch(/not installed/i);\r\n });\r\n\r\n test('status exits 0 and shows counts when installed', () => {\r\n runCLI(['init', '--path', tmpDir, '--skip-update-check']);\r\n const result = runCLI(['status', '--path', tmpDir]);\r\n expect(result.status).toBe(0);\r\n expect(result.stdout).toMatch(/installed/i);\r\n });\r\n});\r\n\r\ndescribe('tribunal-kit help / unknown commands', () => {\r\n test('--help exits 0 and shows usage', () => {\r\n const result = runCLI(['--help']);\r\n expect(result.status).toBe(0);\r\n expect(result.stdout).toMatch(/init|update|status/i);\r\n });\r\n\r\n test('unknown command exits 1', () => {\r\n const result = runCLI(['totally-unknown-command']);\r\n expect(result.status).toBe(1);\r\n expect(result.stderr).toMatch(/unknown command/i);\r\n });\r\n});\r\n"
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/integration/routing.test.js",
|
|
3
|
+
"hash": "aa8ff34289fee5be72e1d3f97083944f8f731dbd",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"path": [],
|
|
8
|
+
"fs": []
|
|
9
|
+
},
|
|
10
|
+
"dependents": [],
|
|
11
|
+
"content": "'use strict';\r\n\r\nconst path = require('path');\r\nconst fs = require('fs');\r\n\r\nconst AGENT_DIR = path.resolve(__dirname, '../../.agent');\r\nconst WORKFLOWS_DIR = path.join(AGENT_DIR, 'workflows');\r\nconst AGENTS_DIR = path.join(AGENT_DIR, 'agents');\r\nconst SKILLS_DIR = path.join(AGENT_DIR, 'skills');\r\n\r\n// Expected slash commands per plan (31 workflows)\r\nconst EXPECTED_WORKFLOWS = [\r\n 'api-tester', 'audit', 'brainstorm', 'changelog', 'create',\r\n 'debug', 'deploy', 'enhance', 'fix', 'generate', 'migrate',\r\n 'orchestrate', 'performance-benchmarker', 'plan', 'preview',\r\n 'refactor', 'review', 'review-ai', 'session', 'status',\r\n 'strengthen-skills', 'swarm', 'test', 'tribunal-backend',\r\n 'tribunal-database', 'tribunal-frontend', 'tribunal-full',\r\n 'tribunal-mobile', 'tribunal-performance', 'tribunal-speed', 'ui-ux-pro-max',\r\n];\r\n\r\ndescribe('Workflow file integrity', () => {\r\n test('.agent/workflows/ directory exists', () => {\r\n expect(fs.existsSync(WORKFLOWS_DIR)).toBe(true);\r\n });\r\n\r\n test('all 31 expected workflow files are present and non-empty', () => {\r\n const missing = [];\r\n const empty = [];\r\n\r\n for (const name of EXPECTED_WORKFLOWS) {\r\n const filePath = path.join(WORKFLOWS_DIR, `${name}.md`);\r\n if (!fs.existsSync(filePath)) {\r\n missing.push(name);\r\n } else {\r\n const size = fs.statSync(filePath).size;\r\n if (size === 0) empty.push(name);\r\n }\r\n }\r\n\r\n expect(missing).toEqual([]);\r\n expect(empty).toEqual([]);\r\n });\r\n\r\n test('no workflow file is a duplicate of another', () => {\r\n const files = fs.readdirSync(WORKFLOWS_DIR).filter(f => f.endsWith('.md'));\r\n const contents = files.map(f =>\r\n fs.readFileSync(path.join(WORKFLOWS_DIR, f), 'utf8').trim()\r\n );\r\n const unique = new Set(contents);\r\n expect(unique.size).toBe(contents.length);\r\n });\r\n});\r\n\r\ndescribe('Agent file integrity', () => {\r\n test('.agent/agents/ directory exists', () => {\r\n expect(fs.existsSync(AGENTS_DIR)).toBe(true);\r\n });\r\n\r\n test('at least 30 agent files are present', () => {\r\n const files = fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md'));\r\n expect(files.length).toBeGreaterThanOrEqual(30);\r\n });\r\n\r\n test('all agent files are non-empty', () => {\r\n const files = fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md'));\r\n const empty = files.filter(f => fs.statSync(path.join(AGENTS_DIR, f)).size === 0);\r\n expect(empty).toEqual([]);\r\n });\r\n});\r\n\r\ndescribe('Skills directory integrity', () => {\r\n test('.agent/skills/ directory exists', () => {\r\n expect(fs.existsSync(SKILLS_DIR)).toBe(true);\r\n });\r\n\r\n test('at least 50 skill entries are present', () => {\r\n const entries = fs.readdirSync(SKILLS_DIR);\r\n // Each skill is a directory containing a SKILL.md\r\n const skillDirs = entries.filter(e =>\r\n fs.statSync(path.join(SKILLS_DIR, e)).isDirectory()\r\n );\r\n expect(skillDirs.length).toBeGreaterThanOrEqual(50);\r\n });\r\n\r\n test('every skill directory contains a SKILL.md', () => {\r\n const entries = fs.readdirSync(SKILLS_DIR);\r\n const skillDirs = entries.filter(e =>\r\n fs.statSync(path.join(SKILLS_DIR, e)).isDirectory()\r\n );\r\n const missing = skillDirs.filter(dir =>\r\n !fs.existsSync(path.join(SKILLS_DIR, dir, 'SKILL.md'))\r\n );\r\n expect(missing).toEqual([]);\r\n });\r\n});\r\n"
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/integration/swarm_dispatcher.test.js",
|
|
3
|
+
"hash": "959cf59504544f624aea7ccad55fe2d9dc25ca4d",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"path": [],
|
|
8
|
+
"fs": [],
|
|
9
|
+
"os": [],
|
|
10
|
+
"../../.agent/scripts/swarm_dispatcher.js": []
|
|
11
|
+
},
|
|
12
|
+
"dependents": [],
|
|
13
|
+
"content": "const path = require('path');\r\nconst fs = require('fs');\r\nconst os = require('os');\r\nconst {\r\n findAgentDir,\r\n validatePayload,\r\n validateWorkerRequest,\r\n validateSwarmPayload\r\n} = require('../../.agent/scripts/swarm_dispatcher.js');\r\n\r\ndescribe('swarm_dispatcher.js legacy mode', () => {\r\n let tmpDir;\r\n\r\n beforeEach(() => {\r\n tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarm-test-'));\r\n });\r\n\r\n afterEach(() => {\r\n fs.rmSync(tmpDir, { recursive: true, force: true });\r\n });\r\n\r\n test('findAgentDir finds .agent directory', () => {\r\n const agentDir = path.join(tmpDir, '.agent');\r\n fs.mkdirSync(agentDir);\r\n const subDir = path.join(tmpDir, 'src', 'deep', 'folder');\r\n fs.mkdirSync(subDir, { recursive: true });\r\n\r\n const result = findAgentDir(subDir);\r\n expect(result).not.toBeNull();\r\n expect(path.resolve(result)).toBe(path.resolve(agentDir));\r\n });\r\n\r\n test('findAgentDir returns null if not found', () => {\r\n const subDir = path.join(tmpDir, 'src', 'deep');\r\n fs.mkdirSync(subDir, { recursive: true });\r\n\r\n expect(findAgentDir(subDir)).toBeNull();\r\n });\r\n\r\n test('validatePayload valid', () => {\r\n const agentsDir = path.join(tmpDir, 'agents');\r\n fs.mkdirSync(agentsDir);\r\n fs.writeFileSync(path.join(agentsDir, 'test_agent.md'), '');\r\n\r\n const workspace = path.join(tmpDir, 'workspace');\r\n fs.mkdirSync(workspace);\r\n fs.writeFileSync(path.join(workspace, 'file1.txt'), '');\r\n\r\n const payload = {\r\n dispatch_micro_workers: [\r\n {\r\n target_agent: \"test_agent\",\r\n files_attached: [\"file1.txt\"]\r\n }\r\n ]\r\n };\r\n\r\n expect(validatePayload(payload, workspace, agentsDir)).toBe(true);\r\n });\r\n\r\n test('validatePayload missing workers', () => {\r\n expect(validatePayload({}, tmpDir, tmpDir)).toBe(false);\r\n });\r\n\r\n test('validatePayload files not a list', () => {\r\n const agentsDir = path.join(tmpDir, 'agents');\r\n fs.mkdirSync(agentsDir);\r\n fs.writeFileSync(path.join(agentsDir, 'test_agent.md'), '');\r\n\r\n const payload = {\r\n dispatch_micro_workers: [\r\n {\r\n target_agent: \"test_agent\",\r\n files_attached: \"a single file string\"\r\n }\r\n ]\r\n };\r\n\r\n expect(validatePayload(payload, tmpDir, agentsDir)).toBe(false);\r\n });\r\n\r\n test('validateSwarmPayload validates WorkerRequest correctly', () => {\r\n const agentsDir = path.join(tmpDir, 'agents');\r\n fs.mkdirSync(agentsDir);\r\n fs.writeFileSync(path.join(agentsDir, 'research.md'), '');\r\n\r\n const req = {\r\n task_id: \"t123\",\r\n type: \"research\",\r\n agent: \"research\",\r\n goal: \"Find the root cause\",\r\n context: \"We have a bug here.\",\r\n max_retries: 2\r\n };\r\n\r\n const errors = validateWorkerRequest(req, 0, agentsDir);\r\n expect(errors).toHaveLength(0);\r\n\r\n expect(validateSwarmPayload([req], agentsDir)).toBe(true);\r\n });\r\n});\r\n"
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/integration/wave2.test.js",
|
|
3
|
+
"hash": "703c635a4a0091e20785c3418b1ab294607a1635",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"fs": [],
|
|
8
|
+
"../../.agent/scripts/skill_integrator.js": [],
|
|
9
|
+
"../../.agent/scripts/skill_evolution.js": [],
|
|
10
|
+
"../../.agent/scripts/case_law_manager.js": []
|
|
11
|
+
},
|
|
12
|
+
"dependents": [],
|
|
13
|
+
"content": "const { getAssociatedScript } = require('../../.agent/scripts/skill_integrator.js');\r\nconst { architecturalWeight, semanticDelta } = require('../../.agent/scripts/skill_evolution.js');\r\nconst { findAgentDir } = require('../../.agent/scripts/case_law_manager.js');\r\n\r\ndescribe('Wave 2 Scripts (Evolving Brain)', () => {\r\n describe('skill_integrator.js', () => {\r\n it('should correctly identify implicit JS scripts', () => {\r\n // we mock fs internally if we wanted to true unit test, but for now just check exports\r\n expect(typeof getAssociatedScript).toBe('function');\r\n });\r\n });\r\n\r\n describe('skill_evolution.js', () => {\r\n it('should assign higher architectural weight to classes and interfaces', () => {\r\n expect(architecturalWeight('class User {')).toBeGreaterThanOrEqual(2);\r\n expect(architecturalWeight('interface Data {')).toBeGreaterThanOrEqual(2);\r\n });\r\n\r\n it('should assign 0 weight to noise patterns like comments and imports', () => {\r\n expect(architecturalWeight('// this is a comment')).toBe(0);\r\n expect(architecturalWeight('import { foo } from \"fs\";')).toBe(0);\r\n expect(architecturalWeight('')).toBe(0);\r\n });\r\n\r\n it('should successfully filter a semantic delta', () => {\r\n const rawDiff = `\r\n@@ -1,3 +1,4 @@\r\n import { foo } from 'fs';\r\n+import { baz } from 'fs';\r\n class Foo {\r\n- // old comment\r\n+ // new comment\r\n+ export class Baz {}\r\n }\r\n `.trim();\r\n\r\n const delta = semanticDelta(rawDiff, 2);\r\n // It gets kept because the hunk contains 'export class Baz {}'\r\n expect(delta).toContain('import { baz }');\r\n expect(delta).toContain('export class Baz {}');\r\n });\r\n });\r\n\r\n describe('case_law_manager.js', () => {\r\n it('should extract tags', () => {\r\n const { extractTags } = require('../../.agent/scripts/case_law_manager.js');\r\n expect(typeof extractTags).toBe('function');\r\n expect(extractTags('hello world and foo bar')).toContain('hello');\r\n expect(extractTags('hello world and foo bar')).not.toContain('and');\r\n });\r\n it('should find agent diff', () => {\r\n expect(typeof findAgentDir).toBe('function');\r\n });\r\n });\r\n});\r\n"
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/unit/args.test.js",
|
|
3
|
+
"hash": "b5863d3402957aae7315537ff9845c99899eeb50",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"../../bin/tribunal-kit": [
|
|
8
|
+
"parseArgs",
|
|
9
|
+
"compareSemver",
|
|
10
|
+
"copyDir",
|
|
11
|
+
"countDir",
|
|
12
|
+
"isSelfInstall",
|
|
13
|
+
"CORE_AGENTS",
|
|
14
|
+
"CORE_SKILLS",
|
|
15
|
+
"generateIDEBridges"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"dependents": [],
|
|
19
|
+
"content": "'use strict';\r\n\r\nconst { parseArgs } = require('../../bin/tribunal-kit');\r\n\r\ndescribe('parseArgs', () => {\r\n test('parses \"init\" command', () => {\r\n const result = parseArgs(['node', 'tribunal-kit.js', 'init']);\r\n expect(result.command).toBe('init');\r\n expect(result.flags).toEqual({});\r\n });\r\n\r\n test('parses \"update\" command', () => {\r\n const result = parseArgs(['node', 'tribunal-kit.js', 'update']);\r\n expect(result.command).toBe('update');\r\n });\r\n\r\n test('parses \"status\" command', () => {\r\n const result = parseArgs(['node', 'tribunal-kit.js', 'status']);\r\n expect(result.command).toBe('status');\r\n });\r\n\r\n test('returns null command when no args given', () => {\r\n const result = parseArgs(['node', 'tribunal-kit.js']);\r\n expect(result.command).toBeNull();\r\n });\r\n\r\n test('parses --force flag', () => {\r\n const result = parseArgs(['node', 'tribunal-kit.js', 'init', '--force']);\r\n expect(result.flags.force).toBe(true);\r\n });\r\n\r\n test('parses --quiet flag', () => {\r\n const result = parseArgs(['node', 'tribunal-kit.js', 'init', '--quiet']);\r\n expect(result.flags.quiet).toBe(true);\r\n });\r\n\r\n test('parses --dry-run flag', () => {\r\n const result = parseArgs(['node', 'tribunal-kit.js', 'init', '--dry-run']);\r\n expect(result.flags.dryRun).toBe(true);\r\n });\r\n\r\n test('parses --skip-update-check flag', () => {\r\n const result = parseArgs(['node', 'tribunal-kit.js', 'init', '--skip-update-check']);\r\n expect(result.flags.skipUpdateCheck).toBe(true);\r\n });\r\n\r\n test('parses --path=<value> flag', () => {\r\n const result = parseArgs(['node', 'tribunal-kit.js', 'init', '--path=./my-app']);\r\n expect(result.flags.path).toBe('./my-app');\r\n });\r\n\r\n test('parses --path <value> flag (space separated)', () => {\r\n const result = parseArgs(['node', 'tribunal-kit.js', 'init', '--path', './my-app']);\r\n expect(result.flags.path).toBe('./my-app');\r\n });\r\n\r\n test('ignores unknown flags gracefully', () => {\r\n const result = parseArgs(['node', 'tribunal-kit.js', 'init', '--unknown-flag']);\r\n expect(result.command).toBe('init');\r\n // Unknown flags don't throw, just get silently ignored\r\n expect(result.flags.unknownFlag).toBeUndefined();\r\n });\r\n\r\n test('multiple flags together', () => {\r\n const result = parseArgs(['node', 'tribunal-kit.js', 'init', '--force', '--quiet', '--dry-run']);\r\n expect(result.flags.force).toBe(true);\r\n expect(result.flags.quiet).toBe(true);\r\n expect(result.flags.dryRun).toBe(true);\r\n });\r\n});\r\n"
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/unit/case_law_manager.test.js",
|
|
3
|
+
"hash": "172bdb2790ea2305d176f44b4d48826343e3fdc0",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"../../.agent/scripts/case_law_manager": []
|
|
8
|
+
},
|
|
9
|
+
"dependents": [],
|
|
10
|
+
"content": "const {\r\n semanticDelta,\r\n extractTags,\r\n isNoiseRejection,\r\n isTrivialLine\r\n} = require('../../.agent/scripts/case_law_manager');\r\n\r\ndescribe('case_law_manager.js', () => {\r\n describe('Noise Filter', () => {\r\n it('should identify noise rejections', () => {\r\n expect(isNoiseRejection('Please fix formatting')).toBe(true);\r\n expect(isNoiseRejection('Trailing whitespace detected')).toBe(true);\r\n expect(isNoiseRejection('Run prettier')).toBe(true);\r\n });\r\n\r\n it('should allow valid rejections', () => {\r\n expect(isNoiseRejection('Missing SQL injection protection')).toBe(false);\r\n expect(isNoiseRejection('Type error in React component')).toBe(false);\r\n });\r\n });\r\n\r\n describe('Semantic Delta', () => {\r\n it('should identify trivial lines', () => {\r\n expect(isTrivialLine('// just a comment')).toBe(true);\r\n expect(isTrivialLine('import { foo } from \"bar\"')).toBe(true);\r\n expect(isTrivialLine('const a = 1;')).toBe(false);\r\n });\r\n\r\n it('should filter trivial changes from diff', () => {\r\n const diff = `--- file.js\\n+++ file.js\\n-import a from 'a'\\n+// a comment\\n+const a = 1;`;\r\n const delta = semanticDelta(diff);\r\n expect(delta).toContain('+const a = 1;');\r\n expect(delta).not.toContain('// a comment');\r\n expect(delta).not.toContain('import a');\r\n });\r\n });\r\n\r\n describe('Extract Tags', () => {\r\n it('should extract meaningful tags and ignore stop words', () => {\r\n const tags = extractTags('The React component has a missing key prop');\r\n expect(tags).toContain('react');\r\n expect(tags).toContain('component');\r\n expect(tags).toContain('missing');\r\n expect(tags).toContain('key');\r\n expect(tags).toContain('prop');\r\n expect(tags).not.toContain('the');\r\n expect(tags).not.toContain('has');\r\n expect(tags).not.toContain('a');\r\n });\r\n });\r\n});\r\n"
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/unit/context_broker.test.js",
|
|
3
|
+
"hash": "5fa154b5885a89376a937bd4825b1ba5643c92f6",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"../../.agent/scripts/context_broker": []
|
|
8
|
+
},
|
|
9
|
+
"dependents": [],
|
|
10
|
+
"content": "/**\n * context_broker.test.js — Unit tests for the Context Density Broker\n * Tests: tokenize, scoreSkill, selectSkills\n */\n\nconst {\n scoreSkill,\n selectSkills,\n tokenize\n} = require('../../.agent/scripts/context_broker');\n\ndescribe('context_broker.js', () => {\n\n // ── tokenize ──────────────────────────────────────────────────────────\n\n describe('tokenize()', () => {\n it('should extract words 3+ characters, lowercased', () => {\n const tokens = tokenize('Build a JWT authentication API');\n expect(tokens).toContain('build');\n expect(tokens).toContain('jwt');\n expect(tokens).toContain('authentication');\n expect(tokens).toContain('api');\n });\n\n it('should ignore 1-2 character words', () => {\n const tokens = tokenize('I am ok to go');\n // \"ok\" is only 2 chars, \"am\" is 2 chars, \"I\" is 1 char, \"to\" is 2 chars, \"go\" is 2 chars\n expect(tokens).toEqual([]);\n });\n\n it('should handle empty string', () => {\n expect(tokenize('')).toEqual([]);\n });\n\n it('should handle underscored identifiers', () => {\n const tokens = tokenize('use_case my_component');\n expect(tokens).toContain('use_case');\n expect(tokens).toContain('my_component');\n });\n });\n\n // ── scoreSkill ────────────────────────────────────────────────────────\n\n describe('scoreSkill()', () => {\n const makeSkill = (name, description = '') => ({\n name,\n description,\n content: ''\n });\n\n it('should give higher scores for domain-matched skills', () => {\n const taskTokens = tokenize('Build a React component with hooks');\n const reactSkill = makeSkill('react-specialist', 'React 19+ specialist. Components, hooks, state.');\n const sqlSkill = makeSkill('sql-pro', 'Advanced SQL queries and optimization');\n\n const reactScore = scoreSkill(reactSkill, 'Build a React component with hooks', [], taskTokens);\n const sqlScore = scoreSkill(sqlSkill, 'Build a React component with hooks', [], taskTokens);\n\n expect(reactScore).toBeGreaterThan(sqlScore);\n });\n\n it('should boost score for file extension affinity', () => {\n const taskTokens = tokenize('Fix the login page');\n const reactSkill = makeSkill('react-specialist', 'React components and hooks');\n\n const withoutExt = scoreSkill(reactSkill, 'Fix the login page', [], taskTokens);\n const withExt = scoreSkill(reactSkill, 'Fix the login page', ['.tsx'], taskTokens);\n\n expect(withExt).toBeGreaterThanOrEqual(withoutExt);\n });\n\n it('should return 0 or low score for completely unrelated skills', () => {\n const taskTokens = tokenize('Deploy to Kubernetes');\n const gameSkill = makeSkill('game-design-expert', 'Game design and player experience');\n\n const score = scoreSkill(gameSkill, 'Deploy to Kubernetes', [], taskTokens);\n expect(score).toBeLessThanOrEqual(1);\n });\n\n it('should boost baseline skills', () => {\n const taskTokens = tokenize('random unrelated task');\n // clean-code is a baseline skill\n const cleanCode = makeSkill('clean-code', 'Clean code mastery');\n\n const score = scoreSkill(cleanCode, 'random unrelated task', [], taskTokens);\n // Baseline skills get +1 regardless of match\n expect(score).toBeGreaterThanOrEqual(1);\n });\n });\n\n // ── selectSkills ──────────────────────────────────────────────────────\n\n describe('selectSkills()', () => {\n // Create minimal mock skills array for testing\n const mockSkills = [\n { name: 'react-specialist', description: 'React 19+ specialist. Components, hooks, state management.', content: '' },\n { name: 'sql-pro', description: 'SQL queries, PostgreSQL, MySQL, optimization', content: '' },\n { name: 'clean-code', description: 'Clean code mastery. Naming, SOLID, refactoring.', content: '' },\n { name: 'testing-patterns', description: 'Testing mastery. Jest, Vitest, Playwright, mocking.', content: '' },\n { name: 'vulnerability-scanner', description: 'Security vulnerability analysis. OWASP, injection.', content: '' },\n ];\n\n it('should return essential, supplementary, and available arrays', () => {\n const result = selectSkills('Build a React component', [], 'large', mockSkills);\n\n expect(result).toHaveProperty('essential');\n expect(result).toHaveProperty('supplementary');\n expect(result).toHaveProperty('available');\n expect(result).toHaveProperty('scores');\n expect(Array.isArray(result.essential)).toBe(true);\n expect(Array.isArray(result.supplementary)).toBe(true);\n expect(Array.isArray(result.available)).toBe(true);\n });\n\n it('should prioritize relevant skills in essential tier', () => {\n const result = selectSkills('Write a SQL query for user analytics', [], 'large', mockSkills);\n const essentialNames = result.essential.map(s => s.name);\n // sql-pro should be ranked highly for a SQL task\n if (essentialNames.length > 0) {\n expect(essentialNames).toContain('sql-pro');\n }\n });\n\n it('should limit essential to 6 for small models', () => {\n const result = selectSkills('Build something', [], 'small', mockSkills);\n expect(result.essential.length).toBeLessThanOrEqual(6);\n // Small model should have empty supplementary\n expect(result.supplementary).toEqual([]);\n });\n\n it('should collapse supplementary into available for small models', () => {\n const result = selectSkills('Build a full-stack app', [], 'small', mockSkills);\n expect(result.supplementary).toEqual([]);\n // Available should contain things that would have been supplementary\n expect(result.available.length).toBeGreaterThanOrEqual(0);\n });\n\n it('should limit essential to 10 for large models', () => {\n const result = selectSkills('Build everything imaginable', [], 'large', mockSkills);\n expect(result.essential.length).toBeLessThanOrEqual(10);\n });\n\n it('should return a scores Map', () => {\n const result = selectSkills('Test something', [], 'large', mockSkills);\n expect(result.scores).toBeInstanceOf(Map);\n expect(result.scores.size).toBe(mockSkills.length);\n });\n });\n});\n"
|
|
11
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/unit/copyDir.test.js",
|
|
3
|
+
"hash": "1f3111ef91650adcb45b728779000c03b9bdcb3f",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"fs": [],
|
|
8
|
+
"path": [],
|
|
9
|
+
"os": [],
|
|
10
|
+
"../../bin/tribunal-kit": [
|
|
11
|
+
"parseArgs",
|
|
12
|
+
"compareSemver",
|
|
13
|
+
"copyDir",
|
|
14
|
+
"countDir",
|
|
15
|
+
"isSelfInstall",
|
|
16
|
+
"CORE_AGENTS",
|
|
17
|
+
"CORE_SKILLS",
|
|
18
|
+
"generateIDEBridges"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"dependents": [],
|
|
22
|
+
"content": "'use strict';\r\n\r\nconst fs = require('fs');\r\nconst path = require('path');\r\nconst os = require('os');\r\nconst { copyDir, countDir } = require('../../bin/tribunal-kit');\r\n\r\nfunction makeTempDir(prefix = 'tk-test-') {\r\n return fs.mkdtempSync(path.join(os.tmpdir(), prefix));\r\n}\r\n\r\ndescribe('countDir', () => {\r\n let tmpDir;\r\n\r\n beforeEach(() => {\r\n tmpDir = makeTempDir();\r\n });\r\n\r\n afterEach(() => {\r\n fs.rmSync(tmpDir, { recursive: true, force: true });\r\n });\r\n\r\n test('returns 0 for empty directory', () => {\r\n expect(countDir(tmpDir)).toBe(0);\r\n });\r\n\r\n test('counts files at top level', () => {\r\n fs.writeFileSync(path.join(tmpDir, 'a.txt'), 'a');\r\n fs.writeFileSync(path.join(tmpDir, 'b.txt'), 'b');\r\n expect(countDir(tmpDir)).toBe(2);\r\n });\r\n\r\n test('counts files recursively in subdirectories', () => {\r\n const sub = path.join(tmpDir, 'sub');\r\n fs.mkdirSync(sub);\r\n fs.writeFileSync(path.join(tmpDir, 'top.txt'), 'top');\r\n fs.writeFileSync(path.join(sub, 'nested.txt'), 'nested');\r\n expect(countDir(tmpDir)).toBe(2);\r\n });\r\n});\r\n\r\ndescribe('copyDir', () => {\r\n let srcDir;\r\n let destDir;\r\n\r\n beforeEach(() => {\r\n srcDir = makeTempDir('tk-src-');\r\n destDir = makeTempDir('tk-dest-');\r\n // Remove destDir so copyDir can create it fresh\r\n fs.rmSync(destDir, { recursive: true, force: true });\r\n });\r\n\r\n afterEach(() => {\r\n fs.rmSync(srcDir, { recursive: true, force: true });\r\n fs.rmSync(destDir, { recursive: true, force: true });\r\n });\r\n\r\n test('copies files to destination', () => {\r\n fs.writeFileSync(path.join(srcDir, 'hello.txt'), 'hello');\r\n copyDir(srcDir, destDir);\r\n expect(fs.existsSync(path.join(destDir, 'hello.txt'))).toBe(true);\r\n expect(fs.readFileSync(path.join(destDir, 'hello.txt'), 'utf8')).toBe('hello');\r\n });\r\n\r\n test('copies nested directories recursively', () => {\r\n const sub = path.join(srcDir, 'sub');\r\n fs.mkdirSync(sub);\r\n fs.writeFileSync(path.join(sub, 'nested.txt'), 'nested');\r\n copyDir(srcDir, destDir);\r\n expect(fs.existsSync(path.join(destDir, 'sub', 'nested.txt'))).toBe(true);\r\n });\r\n\r\n test('returns total file count copied', () => {\r\n fs.writeFileSync(path.join(srcDir, 'a.txt'), 'a');\r\n fs.writeFileSync(path.join(srcDir, 'b.txt'), 'b');\r\n const count = copyDir(srcDir, destDir);\r\n expect(count).toBe(2);\r\n });\r\n\r\n test('dry-run does NOT create destination files', () => {\r\n fs.writeFileSync(path.join(srcDir, 'hello.txt'), 'hello');\r\n const destDryRun = path.join(os.tmpdir(), 'tk-dryrun-' + Date.now());\r\n copyDir(srcDir, destDryRun, true);\r\n expect(fs.existsSync(destDryRun)).toBe(false);\r\n });\r\n\r\n test('dry-run still returns correct file count', () => {\r\n fs.writeFileSync(path.join(srcDir, 'a.txt'), 'a');\r\n fs.writeFileSync(path.join(srcDir, 'b.txt'), 'b');\r\n const destDryRun = path.join(os.tmpdir(), 'tk-dryrun2-' + Date.now());\r\n const count = copyDir(srcDir, destDryRun, true);\r\n expect(count).toBe(2);\r\n });\r\n});\r\n"
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/unit/graph_tools.test.js",
|
|
3
|
+
"hash": "08b5b30f66169e34f86a9b8ad9aa842e0917f218",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"fs": [],
|
|
8
|
+
"path": []
|
|
9
|
+
},
|
|
10
|
+
"dependents": [],
|
|
11
|
+
"content": "const fs = require('fs');\r\nconst path = require('path');\r\n\r\ndescribe('Knowledge Graph Tools', () => {\r\n describe('graph_builder.js', () => {\r\n it('should exist and be executable', () => {\r\n const builderPath = path.join(__dirname, '../../.agent/scripts/graph_builder.js');\r\n expect(fs.existsSync(builderPath)).toBe(true);\r\n const content = fs.readFileSync(builderPath, 'utf8');\r\n expect(content).toContain('function main()');\r\n expect(content).toContain('function walkDir');\r\n expect(content).toContain('isExcluded');\r\n });\r\n });\r\n\r\n describe('graph_zoom.js', () => {\r\n it('should exist and be executable', () => {\r\n const zoomPath = path.join(__dirname, '../../.agent/scripts/graph_zoom.js');\r\n expect(fs.existsSync(zoomPath)).toBe(true);\r\n const content = fs.readFileSync(zoomPath, 'utf8');\r\n expect(content).toContain('function extractSkeleton');\r\n // Ensure fallback logic is present\r\n expect(content).toContain('RAW FILE FALLBACK');\r\n });\r\n });\r\n});\r\n"
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/unit/inner_loop_validator.test.js",
|
|
3
|
+
"hash": "48487187be036a1448dcb2089cb17d01258f964f",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"../../.agent/scripts/inner_loop_validator": []
|
|
8
|
+
},
|
|
9
|
+
"dependents": [],
|
|
10
|
+
"content": "/**\n * inner_loop_validator.test.js — Unit tests for the Inner-Loop Validator\n * Tests: scanCode, computeVerdict, buildSelfHealingInstructions, validate\n */\n\nconst {\n scanCode,\n computeVerdict,\n buildSelfHealingInstructions,\n validate\n} = require('../../.agent/scripts/inner_loop_validator');\n\ndescribe('inner_loop_validator.js', () => {\n\n // ── scanCode ──────────────────────────────────────────────────────────\n\n describe('scanCode()', () => {\n it('should return empty array for clean code', () => {\n const findings = scanCode('const x = 1;\\nconst y = x + 2;\\n');\n expect(findings).toEqual([]);\n });\n\n it('should detect eval() as high severity', () => {\n const findings = scanCode('const result = eval(userInput);');\n const evalFinding = findings.find(f => f.category === 'Code Injection');\n expect(evalFinding).toBeDefined();\n expect(evalFinding.severity).toBe('high');\n expect(evalFinding.line).toBe(1);\n expect(evalFinding.source).toBe('security_scan');\n });\n\n it('should detect innerHTML as high severity XSS', () => {\n const findings = scanCode('el.innerHTML = userInput;');\n const xssFinding = findings.find(f => f.category === 'XSS');\n expect(xssFinding).toBeDefined();\n expect(xssFinding.severity).toBe('high');\n });\n\n it('should detect hardcoded passwords as critical', () => {\n const findings = scanCode('const password = \"supersecret123\";');\n const secretFinding = findings.find(f => f.category === 'Hardcoded Secret');\n expect(secretFinding).toBeDefined();\n expect(secretFinding.severity).toBe('critical');\n });\n\n it('should detect empty catch blocks as medium severity', () => {\n const findings = scanCode('try { foo(); } catch (e) {}');\n const catchFinding = findings.find(f => f.category === 'Error Handling');\n expect(catchFinding).toBeDefined();\n expect(catchFinding.severity).toBe('medium');\n });\n\n it('should detect throw string as low severity', () => {\n const findings = scanCode('throw \"something went wrong\";');\n const throwFinding = findings.find(f => f.category === 'Error Quality');\n expect(throwFinding).toBeDefined();\n expect(throwFinding.severity).toBe('low');\n });\n\n it('should skip lines that are pure comments', () => {\n const code = '// eval(dangerousInput);\\nconst safe = 1;';\n const findings = scanCode(code);\n // eval is in a comment, should not be flagged\n const evalFinding = findings.find(f => f.category === 'Code Injection');\n expect(evalFinding).toBeUndefined();\n });\n\n it('should report correct line numbers', () => {\n const code = 'const a = 1;\\nconst b = 2;\\nconst r = eval(\"bad\");';\n const findings = scanCode(code);\n const evalFinding = findings.find(f => f.category === 'Code Injection');\n expect(evalFinding).toBeDefined();\n expect(evalFinding.line).toBe(3);\n });\n\n it('should detect VERIFY flags as low/informational', () => {\n const findings = scanCode('const x = someApi(); // VERIFY: does this exist?');\n const verifyFinding = findings.find(f => f.category === 'Verification Flag');\n expect(verifyFinding).toBeDefined();\n expect(verifyFinding.severity).toBe('low');\n });\n\n it('should detect browser globals in Node context', () => {\n const findings = scanCode('const w = window.innerWidth;');\n const browserFinding = findings.find(f => f.category === 'Environment Check');\n expect(browserFinding).toBeDefined();\n });\n });\n\n // ── computeVerdict ────────────────────────────────────────────────────\n\n describe('computeVerdict()', () => {\n it('should return APPROVED for no findings', () => {\n const result = computeVerdict([]);\n expect(result.verdict).toBe('APPROVED');\n expect(result.passed).toBe(true);\n });\n\n it('should return APPROVED for low-only findings', () => {\n const findings = [\n { severity: 'low', category: 'Error Quality', line: 1, message: 'test' }\n ];\n const result = computeVerdict(findings);\n expect(result.verdict).toBe('APPROVED');\n expect(result.passed).toBe(true);\n });\n\n it('should return WARNING for medium findings', () => {\n const findings = [\n { severity: 'medium', category: 'Error Handling', line: 1, message: 'test' }\n ];\n const result = computeVerdict(findings);\n expect(result.verdict).toBe('WARNING');\n expect(result.passed).toBe(true);\n });\n\n it('should return REJECTED for high findings', () => {\n const findings = [\n { severity: 'high', category: 'Code Injection', line: 1, message: 'test' }\n ];\n const result = computeVerdict(findings);\n expect(result.verdict).toBe('REJECTED');\n expect(result.passed).toBe(false);\n });\n\n it('should return REJECTED for critical findings', () => {\n const findings = [\n { severity: 'critical', category: 'Hardcoded Secret', line: 1, message: 'test' }\n ];\n const result = computeVerdict(findings);\n expect(result.verdict).toBe('REJECTED');\n expect(result.passed).toBe(false);\n });\n\n it('should use worst severity when mixed findings exist', () => {\n const findings = [\n { severity: 'low', category: 'Info', line: 1, message: 'ok' },\n { severity: 'critical', category: 'Secret', line: 2, message: 'bad' },\n ];\n const result = computeVerdict(findings);\n expect(result.verdict).toBe('REJECTED');\n expect(result.passed).toBe(false);\n });\n\n it('should not let Verification Flags block approval', () => {\n const findings = [\n { severity: 'low', category: 'Verification Flag', line: 1, message: 'verify' }\n ];\n const result = computeVerdict(findings);\n expect(result.verdict).toBe('APPROVED');\n expect(result.passed).toBe(true);\n });\n });\n\n // ── buildSelfHealingInstructions ───────────────────────────────────────\n\n describe('buildSelfHealingInstructions()', () => {\n it('should return null when no blocking findings', () => {\n const result = buildSelfHealingInstructions([\n { severity: 'low', category: 'Info', line: 1, message: 'test', fix: 'do nothing' }\n ]);\n expect(result).toBeNull();\n });\n\n it('should return null for empty array', () => {\n expect(buildSelfHealingInstructions([])).toBeNull();\n });\n\n it('should generate instructions for critical findings', () => {\n const result = buildSelfHealingInstructions([\n { severity: 'critical', category: 'Hardcoded Secret', line: 5, message: 'password hardcoded', fix: 'Use env vars' }\n ]);\n expect(result).toContain('CRITICAL');\n expect(result).toContain('Line 5');\n expect(result).toContain('Hardcoded Secret');\n expect(result).toContain('Use env vars');\n });\n\n it('should generate instructions for high findings', () => {\n const result = buildSelfHealingInstructions([\n { severity: 'high', category: 'Code Injection', line: 3, message: 'eval used', fix: 'Remove eval' }\n ]);\n expect(result).toContain('HIGH');\n expect(result).toContain('Line 3');\n });\n\n it('should skip medium findings in instructions', () => {\n const result = buildSelfHealingInstructions([\n { severity: 'medium', category: 'Error Handling', line: 1, message: 'empty catch', fix: 'Add handler' }\n ]);\n expect(result).toBeNull();\n });\n });\n\n // ── validate (integration of scan + verdict + healing) ────────────────\n\n describe('validate()', () => {\n it('should return APPROVED for clean code', () => {\n const result = validate('const x = 1;\\nconst y = x + 2;');\n expect(result.verdict).toBe('APPROVED');\n expect(result.passed).toBe(true);\n expect(result.issues).toEqual([]);\n expect(result.self_healing_instructions).toBeNull();\n });\n\n it('should return REJECTED for code with eval', () => {\n const result = validate('const x = eval(input);');\n expect(result.verdict).toBe('REJECTED');\n expect(result.passed).toBe(false);\n expect(result.issues.length).toBeGreaterThan(0);\n expect(result.self_healing_instructions).not.toBeNull();\n });\n\n it('should handle empty/null input gracefully', () => {\n const result = validate('');\n expect(result.verdict).toBe('APPROVED');\n expect(result.passed).toBe(true);\n expect(result.summary).toContain('No code provided');\n });\n\n it('should handle null input', () => {\n const result = validate(null);\n expect(result.verdict).toBe('APPROVED');\n expect(result.passed).toBe(true);\n });\n\n it('should include meta with line count and timestamp', () => {\n const result = validate('const a = 1;\\nconst b = 2;\\nconst c = 3;');\n expect(result.meta).toBeDefined();\n expect(result.meta.lines_scanned).toBe(3);\n expect(result.meta.lang).toBe('js');\n expect(result.meta.timestamp).toBeDefined();\n });\n\n it('should sort issues by severity (critical first)', () => {\n const code = [\n 'throw \"bad\";', // low: Error Quality\n 'const password = \"secret123\";', // critical: Hardcoded Secret\n 'const x = eval(input);', // high: Code Injection\n ].join('\\n');\n const result = validate(code);\n const severities = result.issues.map(i => i.severity);\n const rankOrder = { critical: 0, high: 1, medium: 2, low: 3 };\n for (let i = 1; i < severities.length; i++) {\n expect(rankOrder[severities[i]]).toBeGreaterThanOrEqual(rankOrder[severities[i - 1]]);\n }\n });\n\n it('should generate summary with issue counts', () => {\n const result = validate('const password = \"secret\";');\n expect(result.summary).toBeTruthy();\n expect(result.summary).toContain('critical');\n });\n });\n});\n"
|
|
11
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/unit/selfInstall.test.js",
|
|
3
|
+
"hash": "65b099af8b884c6c1d174f7f70c6d836c27e02ea",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"path": [],
|
|
8
|
+
"fs": [],
|
|
9
|
+
"os": [],
|
|
10
|
+
"../../bin/tribunal-kit": [
|
|
11
|
+
"parseArgs",
|
|
12
|
+
"compareSemver",
|
|
13
|
+
"copyDir",
|
|
14
|
+
"countDir",
|
|
15
|
+
"isSelfInstall",
|
|
16
|
+
"CORE_AGENTS",
|
|
17
|
+
"CORE_SKILLS",
|
|
18
|
+
"generateIDEBridges"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"dependents": [],
|
|
22
|
+
"content": "'use strict';\r\n\r\nconst path = require('path');\r\nconst fs = require('fs');\r\nconst os = require('os');\r\nconst { isSelfInstall } = require('../../bin/tribunal-kit');\r\n\r\ndescribe('isSelfInstall', () => {\r\n test('returns true when target is the kit root (package root)', () => {\r\n // The kit root is one level above bin/\r\n const kitRoot = path.resolve(__dirname, '../../');\r\n expect(isSelfInstall(kitRoot)).toBe(true);\r\n });\r\n\r\n test('returns false for an unrelated directory', () => {\r\n expect(isSelfInstall(os.tmpdir())).toBe(false);\r\n });\r\n\r\n test('returns true when target package.json has name \"tribunal-kit\"', () => {\r\n // Create a temp dir with a fake package.json mimicking the kit\r\n const fakeKitDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-tk-'));\r\n fs.writeFileSync(\r\n path.join(fakeKitDir, 'package.json'),\r\n JSON.stringify({ name: 'tribunal-kit', version: '1.0.0' })\r\n );\r\n expect(isSelfInstall(fakeKitDir)).toBe(true);\r\n fs.rmSync(fakeKitDir, { recursive: true, force: true });\r\n });\r\n\r\n test('returns false when target package.json has a different name', () => {\r\n const fakeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'other-pkg-'));\r\n fs.writeFileSync(\r\n path.join(fakeDir, 'package.json'),\r\n JSON.stringify({ name: 'my-other-project', version: '1.0.0' })\r\n );\r\n expect(isSelfInstall(fakeDir)).toBe(false);\r\n fs.rmSync(fakeDir, { recursive: true, force: true });\r\n });\r\n\r\n test('returns false when target has no package.json', () => {\r\n const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'no-pkg-'));\r\n expect(isSelfInstall(emptyDir)).toBe(false);\r\n fs.rmSync(emptyDir, { recursive: true, force: true });\r\n });\r\n});\r\n"
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/unit/semver.test.js",
|
|
3
|
+
"hash": "d3b4e92656e66f496f034174abd53012ed00b33c",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"../../bin/tribunal-kit": [
|
|
8
|
+
"parseArgs",
|
|
9
|
+
"compareSemver",
|
|
10
|
+
"copyDir",
|
|
11
|
+
"countDir",
|
|
12
|
+
"isSelfInstall",
|
|
13
|
+
"CORE_AGENTS",
|
|
14
|
+
"CORE_SKILLS",
|
|
15
|
+
"generateIDEBridges"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"dependents": [],
|
|
19
|
+
"content": "'use strict';\r\n\r\nconst { compareSemver } = require('../../bin/tribunal-kit');\r\n\r\ndescribe('compareSemver', () => {\r\n test('returns 1 when a > b (patch)', () => {\r\n expect(compareSemver('2.4.2', '2.4.1')).toBe(1);\r\n });\r\n\r\n test('returns 1 when a > b (minor)', () => {\r\n expect(compareSemver('2.5.0', '2.4.9')).toBe(1);\r\n });\r\n\r\n test('returns 1 when a > b (major)', () => {\r\n expect(compareSemver('3.0.0', '2.9.9')).toBe(1);\r\n });\r\n\r\n test('returns 0 when a === b', () => {\r\n expect(compareSemver('2.4.2', '2.4.2')).toBe(0);\r\n });\r\n\r\n test('returns -1 when a < b (patch)', () => {\r\n expect(compareSemver('2.4.1', '2.4.2')).toBe(-1);\r\n });\r\n\r\n test('returns -1 when a < b (minor)', () => {\r\n expect(compareSemver('2.4.9', '2.5.0')).toBe(-1);\r\n });\r\n\r\n test('strips leading \"v\" prefix from a', () => {\r\n expect(compareSemver('v2.4.2', '2.4.2')).toBe(0);\r\n });\r\n\r\n test('strips leading \"v\" prefix from b', () => {\r\n expect(compareSemver('2.4.2', 'v2.4.2')).toBe(0);\r\n });\r\n\r\n test('strips leading \"v\" from both', () => {\r\n expect(compareSemver('v2.4.3', 'v2.4.2')).toBe(1);\r\n });\r\n\r\n test('handles missing patch segment (treats as 0)', () => {\r\n expect(compareSemver('2.4', '2.4.0')).toBe(0);\r\n });\r\n});\r\n"
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file": "test/unit/swarm_dispatcher.test.js",
|
|
3
|
+
"hash": "59ea8d2f8e73dd3a384df1966e55002db2df0947",
|
|
4
|
+
"riskScore": "Low",
|
|
5
|
+
"blastRadius": 0,
|
|
6
|
+
"imports": {
|
|
7
|
+
"path": [],
|
|
8
|
+
"fs": []
|
|
9
|
+
},
|
|
10
|
+
"dependents": [],
|
|
11
|
+
"content": "const path = require('path');\r\nconst fs = require('fs');\r\n\r\ndescribe('swarm_dispatcher.js', () => {\r\n it('should be a valid javascript file', () => {\r\n const filePath = path.join(__dirname, '../../.agent/scripts/swarm_dispatcher.js');\r\n expect(fs.existsSync(filePath)).toBe(true);\r\n const content = fs.readFileSync(filePath, 'utf8');\r\n expect(content.includes('module.exports')).toBeTruthy();\r\n });\r\n \r\n // As swarm_dispatcher heavily relies on filesystem operations and child_process,\r\n // we would use jest.mock('child_process') and jest.mock('fs') for deeper testing.\r\n // This serves as the foundation for the swarm test suite.\r\n});\r\n"
|
|
12
|
+
}
|
|
@@ -1,18 +1,170 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const GREEN
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
/**
|
|
2
|
+
* _colors.js — Tribunal Kit ANSI Color System
|
|
3
|
+
* ════════════════════════════════════════════════════════════════
|
|
4
|
+
* Single source of truth for all terminal color constants.
|
|
5
|
+
* Import this module instead of duplicating color codes.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { RED, GREEN, BOLD, RESET } = require('./_colors');
|
|
9
|
+
* const C = require('./_colors'); // C.RED, C.banner('Title'), etc.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
// ── Core ANSI Escape Codes ──────────────────────────────────────────────────
|
|
15
|
+
const GREEN = '\x1b[92m';
|
|
16
|
+
const YELLOW = '\x1b[93m';
|
|
17
|
+
const CYAN = '\x1b[96m';
|
|
18
|
+
const RED = '\x1b[91m';
|
|
19
|
+
const BLUE = '\x1b[94m';
|
|
20
|
+
const MAGENTA = '\x1b[95m';
|
|
21
|
+
const WHITE = '\x1b[97m';
|
|
22
|
+
const GRAY = '\x1b[90m';
|
|
23
|
+
const BOLD = '\x1b[1m';
|
|
24
|
+
const DIM = '\x1b[2m';
|
|
25
|
+
const ITALIC = '\x1b[3m';
|
|
26
|
+
const UNDERLINE = '\x1b[4m';
|
|
27
|
+
const RESET = '\x1b[0m';
|
|
28
|
+
|
|
29
|
+
// ── Box Drawing Characters ──────────────────────────────────────────────────
|
|
30
|
+
const BOX = {
|
|
31
|
+
TL: '╔', TR: '╗', BL: '╚', BR: '╝',
|
|
32
|
+
H: '═', V: '║',
|
|
33
|
+
tl: '┌', tr: '┐', bl: '└', br: '┘',
|
|
34
|
+
h: '─', v: '│',
|
|
35
|
+
cross: '┼', teeL: '├', teeR: '┤', teeT: '┬', teeB: '┴',
|
|
36
|
+
bullet: '●', bulletEmpty: '○',
|
|
37
|
+
arrow: '→', arrowLeft: '←',
|
|
38
|
+
check: '✔', cross_mark: '✖',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ── Branded Output Helpers ──────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generate a branded Tribunal banner box.
|
|
45
|
+
* @param {string} title - Script title (e.g., "verify_all.js")
|
|
46
|
+
* @param {object} [meta] - Optional metadata lines { key: value }
|
|
47
|
+
* @returns {string} Formatted banner string
|
|
48
|
+
*/
|
|
49
|
+
function banner(title, meta = {}) {
|
|
50
|
+
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
51
|
+
const lines = [
|
|
52
|
+
` ${BOLD}${CYAN}⚖️ TRIBUNAL KIT${RESET} ${DIM}— ${title}${RESET}`,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const metaEntries = Object.entries(meta);
|
|
56
|
+
if (metaEntries.length > 0) {
|
|
57
|
+
for (const [key, value] of metaEntries) {
|
|
58
|
+
lines.push(` ${DIM}${key}:${RESET} ${value}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
lines.push(` ${DIM}${timestamp}${RESET}`);
|
|
62
|
+
|
|
63
|
+
const divider = `${CYAN}${'━'.repeat(56)}${RESET}`;
|
|
64
|
+
return `\n${divider}\n${lines.join('\n')}\n${divider}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generate a section header with optional timing.
|
|
69
|
+
* @param {string} title - Section title
|
|
70
|
+
* @param {number} [index] - Step number (1-based)
|
|
71
|
+
* @param {number} [ms] - Execution time in ms
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function sectionHeader(title, index, ms) {
|
|
75
|
+
const num = index != null ? `${index} — ` : '';
|
|
76
|
+
const timing = ms != null ? `${DIM} (${formatMs(ms)})${RESET}` : '';
|
|
77
|
+
return `\n${BOLD}${BLUE}━━━ ${num}${title} ━━━${RESET}${timing}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Format milliseconds into a human-readable string.
|
|
82
|
+
* @param {number} ms
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
function formatMs(ms) {
|
|
86
|
+
if (ms < 1000) return `${ms}ms`;
|
|
87
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
88
|
+
return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Status Indicator Helpers ────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/** @param {string} msg */
|
|
94
|
+
function ok(msg) { console.log(` ${GREEN}✅ ${msg}${RESET}`); }
|
|
95
|
+
|
|
96
|
+
/** @param {string} msg */
|
|
97
|
+
function fail(msg) { console.log(` ${RED}❌ ${msg}${RESET}`); }
|
|
98
|
+
|
|
99
|
+
/** @param {string} msg */
|
|
100
|
+
function warn(msg) { console.log(` ${YELLOW}⚠️ ${msg}${RESET}`); }
|
|
101
|
+
|
|
102
|
+
/** @param {string} msg */
|
|
103
|
+
function skip(msg) { console.log(` ${YELLOW}⏭️ ${msg}${RESET}`); }
|
|
104
|
+
|
|
105
|
+
/** @param {string} msg */
|
|
106
|
+
function info(msg) { console.log(` ${BLUE}ℹ️ ${msg}${RESET}`); }
|
|
107
|
+
|
|
108
|
+
// ── Summary Table Builder ───────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Build and print a box-drawing summary table.
|
|
112
|
+
* @param {Array<{name: string, status: 'pass'|'fail'|'skip'|'warn', ms?: number}>} rows
|
|
113
|
+
*/
|
|
114
|
+
function summaryTable(rows) {
|
|
115
|
+
const maxName = Math.max(...rows.map(r => r.name.length), 18);
|
|
116
|
+
const colW = maxName + 2;
|
|
117
|
+
|
|
118
|
+
const statusIcon = (s) => {
|
|
119
|
+
if (s === 'pass') return `${GREEN} ✅ ${RESET}`;
|
|
120
|
+
if (s === 'fail') return `${RED} ❌ ${RESET}`;
|
|
121
|
+
if (s === 'skip') return `${YELLOW} ⏭️ ${RESET}`;
|
|
122
|
+
if (s === 'warn') return `${YELLOW} ⚠️ ${RESET}`;
|
|
123
|
+
return `${DIM} — ${RESET}`;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const pad = (str, len) => str + ' '.repeat(Math.max(0, len - str.length));
|
|
127
|
+
const top = ` ${BOX.tl}${BOX.h.repeat(colW)}${BOX.teeT}${BOX.h.repeat(6)}${BOX.teeT}${BOX.h.repeat(10)}${BOX.tr}`;
|
|
128
|
+
const mid = ` ${BOX.teeL}${BOX.h.repeat(colW)}${BOX.cross}${BOX.h.repeat(6)}${BOX.cross}${BOX.h.repeat(10)}${BOX.teeR}`;
|
|
129
|
+
const bot = ` ${BOX.bl}${BOX.h.repeat(colW)}${BOX.teeB}${BOX.h.repeat(6)}${BOX.teeB}${BOX.h.repeat(10)}${BOX.br}`;
|
|
130
|
+
const hdr = ` ${BOX.v} ${BOLD}${pad('Check', colW - 2)}${RESET} ${BOX.v}${BOLD} Res ${RESET}${BOX.v} ${BOLD}${pad('Time', 8)}${RESET}${BOX.v}`;
|
|
131
|
+
|
|
132
|
+
console.log(top);
|
|
133
|
+
console.log(hdr);
|
|
134
|
+
console.log(mid);
|
|
135
|
+
|
|
136
|
+
for (const row of rows) {
|
|
137
|
+
const time = row.ms != null ? formatMs(row.ms) : '—';
|
|
138
|
+
const line = ` ${BOX.v} ${pad(row.name, colW - 2)} ${BOX.v}${statusIcon(row.status)}${BOX.v} ${pad(time, 8)}${BOX.v}`;
|
|
139
|
+
console.log(line);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(bot);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── High-Precision Timer ────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create a timer. Call returned function to get elapsed ms.
|
|
149
|
+
* @returns {function(): number}
|
|
150
|
+
*/
|
|
151
|
+
function timer() {
|
|
152
|
+
const start = process.hrtime.bigint();
|
|
153
|
+
return () => Number(process.hrtime.bigint() - start) / 1e6;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
// Core colors
|
|
158
|
+
GREEN, YELLOW, CYAN, RED, BLUE, MAGENTA, WHITE, GRAY,
|
|
159
|
+
BOLD, DIM, ITALIC, UNDERLINE, RESET,
|
|
160
|
+
// Box drawing
|
|
161
|
+
BOX,
|
|
162
|
+
// Branded helpers
|
|
163
|
+
banner, sectionHeader, formatMs,
|
|
164
|
+
// Status indicators
|
|
165
|
+
ok, fail, warn, skip, info,
|
|
166
|
+
// Summary
|
|
167
|
+
summaryTable,
|
|
168
|
+
// Timing
|
|
169
|
+
timer,
|
|
170
|
+
};
|