kanbango 3.4.1 → 3.6.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.
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fake opencode runner for workflow-gates tests.
4
+ * Env:
5
+ * FAKE_OPENCODE_MODE = pass|fail|blocked|gate_pass|gate_fail|gate_missing|hang|exit1
6
+ * FAKE_OPENCODE_REVIEW_MODE = gate_pass|gate_fail|gate_missing (optional; overrides when --agent is review)
7
+ * FAKE_OPENCODE_ARGV_FILE = path to write JSON argv
8
+ * FAKE_OPENCODE_HANG_MS = hang duration (default 60000)
9
+ */
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const args = process.argv.slice(2);
14
+ const agentIdx = args.indexOf('--agent');
15
+ const agent = agentIdx >= 0 ? args[agentIdx + 1] : '';
16
+
17
+ let mode = String(process.env.FAKE_OPENCODE_MODE || 'pass').trim();
18
+ const reviewMode = String(process.env.FAKE_OPENCODE_REVIEW_MODE || '').trim();
19
+ // Review agents: prefer review mode, else map testing modes to gate_* defaults.
20
+ if (agent === 'temida' || agent === 'review' || /temida|review/i.test(agent)) {
21
+ if (reviewMode) {
22
+ mode = reviewMode;
23
+ } else if (mode === 'pass') {
24
+ mode = 'gate_pass';
25
+ } else if (mode === 'fail') {
26
+ mode = 'gate_fail';
27
+ } else if (mode === 'blocked' || mode === 'gate_missing') {
28
+ mode = mode === 'blocked' ? 'gate_missing' : mode;
29
+ }
30
+ }
31
+
32
+ const argvFile = process.env.FAKE_OPENCODE_ARGV_FILE;
33
+ if (argvFile) {
34
+ try {
35
+ fs.mkdirSync(path.dirname(argvFile), { recursive: true });
36
+ fs.writeFileSync(argvFile, JSON.stringify(args, null, 2) + '\n', 'utf-8');
37
+ } catch (err) {
38
+ process.stderr.write(`argv write failed: ${err.message}\n`);
39
+ }
40
+ }
41
+
42
+ if (mode === 'hang') {
43
+ const ms = Number(process.env.FAKE_OPENCODE_HANG_MS || 60000);
44
+ setTimeout(() => process.exit(0), ms);
45
+ return;
46
+ }
47
+
48
+ const outputs = {
49
+ pass: { out: 'Tests green\nPASS\n', code: 0 },
50
+ fail: { out: 'Tests red\nFAIL\n', code: 1 },
51
+ blocked: { out: 'Missing env\nBLOCKED\n', code: 0 },
52
+ gate_pass: {
53
+ out: 'Nagroda: solidny diff.\nKara kosmetyczna: naming.\nWedka: ok.\nGATE: PASS\n',
54
+ code: 0
55
+ },
56
+ gate_fail: {
57
+ out: 'Kara: brak testow produkcji.\nWedka: dodaj testy.\nGATE: FAIL\n',
58
+ code: 0
59
+ },
60
+ gate_missing: {
61
+ out: 'Wyrok bez markera bramki.\nTylko narracja.\n',
62
+ code: 0
63
+ },
64
+ exit1: { out: 'crashed without verdict\n', code: 1 }
65
+ };
66
+
67
+ const picked = outputs[mode] || outputs.pass;
68
+ process.stdout.write(picked.out);
69
+ process.exit(picked.code);
package/tests/index.js ADDED
@@ -0,0 +1,19 @@
1
+ const assert = require('assert');
2
+ const path = require('path');
3
+
4
+ function run() {
5
+ const pkg = require(path.join(__dirname, '..', 'index.js'));
6
+ assert.ok(pkg.kanban, 'index exports kanban');
7
+ assert.ok(pkg.plan, 'index exports plan');
8
+ assert.ok(pkg.workflow, 'index exports workflow');
9
+ assert.ok(pkg.guiRegistry, 'index exports guiRegistry');
10
+ assert.ok(pkg.playbook, 'index exports playbook');
11
+ assert.ok(pkg.kanban.COLS.includes('testing'));
12
+ assert.ok(pkg.kanban.COLS.includes('review'));
13
+ assert.strictEqual(typeof pkg.workflow.loadConfig, 'function');
14
+ assert.strictEqual(typeof pkg.workflow.maybeEnqueueOnColumnEnter, 'function');
15
+ assert.strictEqual(typeof pkg.plan.done, 'function');
16
+ console.log('✓ index.test.js passed');
17
+ }
18
+
19
+ run();
@@ -0,0 +1,118 @@
1
+ const assert = require('assert');
2
+ const crypto = require('crypto');
3
+ const fs = require('fs').promises;
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ const PKG = path.join(__dirname, '..');
9
+ const CLI = path.join(PKG, 'bin', 'kanban.js');
10
+ const SRC_QA = path.join(PKG, 'agents', 'qa-tester.md');
11
+
12
+ function runCli(cwd, args) {
13
+ return spawnSync(process.execPath, [CLI, ...args], {
14
+ cwd,
15
+ encoding: 'utf-8'
16
+ });
17
+ }
18
+
19
+ function sha256(content) {
20
+ return crypto.createHash('sha256').update(content).digest('hex');
21
+ }
22
+
23
+ async function run() {
24
+ // init creates testing/review dirs + README
25
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'kanbango-bin-'));
26
+ const init = runCli(root, ['init']);
27
+ assert.strictEqual(init.status, 0, init.stderr);
28
+ for (const col of ['testing', 'review', 'active', 'planned', 'icebox', 'done']) {
29
+ const st = await fs.stat(path.join(root, 'backlog', col));
30
+ assert.ok(st.isDirectory(), col);
31
+ }
32
+ const readme = await fs.readFile(path.join(root, 'backlog', 'README.md'), 'utf-8');
33
+ assert.ok(readme.includes('testing/'));
34
+ assert.ok(readme.includes('review/'));
35
+
36
+ // mcp-init --opencode copies agents + writes manifest
37
+ const ocRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kanbango-bin-oc-'));
38
+ const oc = runCli(ocRoot, ['mcp-init', '--opencode']);
39
+ assert.strictEqual(oc.status, 0, oc.stderr);
40
+ const agentDir = path.join(ocRoot, '.opencode', 'agent');
41
+ const agents = await fs.readdir(agentDir);
42
+ assert.ok(agents.includes('qa-tester.md'));
43
+ assert.ok(agents.includes('temida.md'));
44
+ assert.ok(agents.includes('.kanbango-agents.json'));
45
+ const temida = await fs.readFile(path.join(agentDir, 'temida.md'), 'utf-8');
46
+ assert.ok(temida.includes('GATE: PASS'));
47
+
48
+ const srcQa = await fs.readFile(SRC_QA, 'utf-8');
49
+ const srcHash = sha256(srcQa);
50
+ const manifest1 = JSON.parse(await fs.readFile(path.join(agentDir, '.kanbango-agents.json'), 'utf-8'));
51
+ assert.strictEqual(manifest1.agents['qa-tester.md'], srcHash);
52
+
53
+ // second run, identical source → unchanged
54
+ const againSame = runCli(ocRoot, ['mcp-init', '--opencode']);
55
+ assert.strictEqual(againSame.status, 0);
56
+ assert.ok(againSame.stdout.includes('Bez zmian') || againSame.stdout.includes('unchanged')
57
+ || againSame.stdout.includes('Pominięto') === false);
58
+ assert.ok(againSame.stdout.includes('Bez zmian .opencode/agent/qa-tester.md'));
59
+ assert.strictEqual(await fs.readFile(path.join(agentDir, 'qa-tester.md'), 'utf-8'), srcQa);
60
+
61
+ // local edit while manifest matches previous package hash → conflict, keep local
62
+ await fs.writeFile(path.join(agentDir, 'qa-tester.md'), 'KEEP_LOCAL_EDIT', 'utf-8');
63
+ const conflictRun = runCli(ocRoot, ['mcp-init', '--opencode']);
64
+ assert.strictEqual(conflictRun.status, 0);
65
+ assert.ok(
66
+ conflictRun.stdout.includes('Konflikt') || conflictRun.stdout.includes('Pominięto'),
67
+ `expected conflict/skip message, got: ${conflictRun.stdout}`
68
+ );
69
+ assert.strictEqual(
70
+ await fs.readFile(path.join(agentDir, 'qa-tester.md'), 'utf-8'),
71
+ 'KEEP_LOCAL_EDIT'
72
+ );
73
+
74
+ // clean older package version: dest matches recorded hash, source "new" → auto update
75
+ // Simulate by writing dest = old body, manifest hash = old hash, then force-path via
76
+ // restoring package content is fixed; instead write dest to old body with matching manifest.
77
+ const oldBody = 'OLD_PACKAGE_VERSION\n';
78
+ const oldHash = sha256(oldBody);
79
+ await fs.writeFile(path.join(agentDir, 'qa-tester.md'), oldBody, 'utf-8');
80
+ const man = JSON.parse(await fs.readFile(path.join(agentDir, '.kanbango-agents.json'), 'utf-8'));
81
+ man.agents['qa-tester.md'] = oldHash;
82
+ await fs.writeFile(
83
+ path.join(agentDir, '.kanbango-agents.json'),
84
+ JSON.stringify(man, null, 2) + '\n',
85
+ 'utf-8'
86
+ );
87
+ const autoUpdate = runCli(ocRoot, ['mcp-init', '--opencode']);
88
+ assert.strictEqual(autoUpdate.status, 0, autoUpdate.stderr);
89
+ assert.ok(
90
+ autoUpdate.stdout.includes('Zaktualizowano .opencode/agent/qa-tester.md'),
91
+ autoUpdate.stdout
92
+ );
93
+ assert.strictEqual(await fs.readFile(path.join(agentDir, 'qa-tester.md'), 'utf-8'), srcQa);
94
+ const manAfter = JSON.parse(await fs.readFile(path.join(agentDir, '.kanbango-agents.json'), 'utf-8'));
95
+ assert.strictEqual(manAfter.agents['qa-tester.md'], srcHash);
96
+
97
+ // --force overwrites local edit
98
+ await fs.writeFile(path.join(agentDir, 'qa-tester.md'), 'FORCE_ME', 'utf-8');
99
+ const forceRun = runCli(ocRoot, ['mcp-init', '--opencode', '--force']);
100
+ assert.strictEqual(forceRun.status, 0);
101
+ assert.strictEqual(await fs.readFile(path.join(agentDir, 'qa-tester.md'), 'utf-8'), srcQa);
102
+
103
+ // --claude does not create .opencode/agent
104
+ const claudeRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kanbango-bin-cl-'));
105
+ const cl = runCli(claudeRoot, ['mcp-init', '--claude']);
106
+ assert.strictEqual(cl.status, 0, cl.stderr);
107
+ await assert.rejects(
108
+ () => fs.access(path.join(claudeRoot, '.opencode', 'agent')),
109
+ (err) => err.code === 'ENOENT'
110
+ );
111
+
112
+ console.log('✓ bin-kanban.test.js passed');
113
+ }
114
+
115
+ run().catch((err) => {
116
+ console.error(err);
117
+ process.exit(1);
118
+ });
@@ -0,0 +1,104 @@
1
+ const assert = require('assert');
2
+ const fs = require('fs').promises;
3
+ const os = require('os');
4
+ const path = require('path');
5
+
6
+ async function run() {
7
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kanbango-kanban-'));
8
+ process.chdir(tempRoot);
9
+ const kanban = require(path.join(__dirname, '..', 'kanban.js'));
10
+
11
+ assert.ok(kanban.COLS.includes('testing'));
12
+ assert.ok(kanban.COLS.includes('review'));
13
+ assert.strictEqual(kanban.STATUS_MAP.testing, 'testing');
14
+ assert.strictEqual(kanban.STATUS_MAP.review, 'review');
15
+
16
+ await kanban.ensureBacklogDir();
17
+ for (const col of ['testing', 'review']) {
18
+ const st = await fs.stat(path.join(tempRoot, 'backlog', col));
19
+ assert.ok(st.isDirectory());
20
+ }
21
+
22
+ const ev = kanban.normalizeEvidence([{
23
+ diff: 'old',
24
+ test_command: 't',
25
+ stdout: '',
26
+ stderr: '',
27
+ exit_code: 0,
28
+ stage: 'testing',
29
+ agent: 'qa-tester',
30
+ verdict: 'pass',
31
+ summary: 'ok'
32
+ }]);
33
+ assert.strictEqual(ev[0].stage, 'testing');
34
+ assert.strictEqual(ev[0].verdict, 'pass');
35
+
36
+ const wf = kanban.normalizeWorkflow({
37
+ stage: 'review',
38
+ status: 'running',
39
+ agent: 'temida',
40
+ run_id: 'run-x'
41
+ });
42
+ assert.strictEqual(wf.stage, 'review');
43
+ assert.strictEqual(wf.status, 'running');
44
+
45
+ const task = await kanban.doCreate('Kanban unit', 'active', '—', {});
46
+ const moved = await kanban.updateTask(task.id, { column: 'testing' });
47
+ assert.strictEqual(moved.column, 'testing');
48
+
49
+ await assert.rejects(
50
+ () => kanban.updateTask(task.id, { column: 'done' }),
51
+ (err) => {
52
+ assert.strictEqual(err.code, 'INVALID_TRANSITION');
53
+ assert.deepStrictEqual(err.details.allowed_columns, ['active', 'review']);
54
+ assert.strictEqual(err.details.from, 'testing');
55
+ assert.strictEqual(err.details.to, 'done');
56
+ assert.ok(err.hint.includes('active, review'));
57
+ return true;
58
+ }
59
+ );
60
+ assert.deepStrictEqual(kanban.allowedColumnsFrom('active'), ['planned', 'testing', 'icebox']);
61
+ assert.deepStrictEqual(kanban.allowedColumnsFrom('review'), ['active', 'done']);
62
+ kanban.validateTransition('active', 'testing', '001');
63
+ kanban.validateTransition('testing', 'testing', '001');
64
+
65
+ const appended = await kanban.updateTask(task.id, {
66
+ appendEvidence: {
67
+ diff: '',
68
+ test_command: 'fake',
69
+ stdout: 'PASS',
70
+ stderr: '',
71
+ exit_code: 0,
72
+ stage: 'testing',
73
+ agent: 'qa-tester',
74
+ verdict: 'pass',
75
+ summary: 'PASS'
76
+ },
77
+ workflow: {
78
+ stage: 'testing',
79
+ status: 'pass',
80
+ agent: 'qa-tester',
81
+ run_id: 'run-1'
82
+ }
83
+ });
84
+ assert.ok(appended.evidence.some((e) => e.verdict === 'pass'));
85
+ assert.strictEqual(appended.workflow.status, 'pass');
86
+
87
+ assert.strictEqual(
88
+ kanban.deriveEpicStatus([{ column: 'testing' }], {}),
89
+ 'active'
90
+ );
91
+ const progress = kanban.getEpicProgress([
92
+ { column: 'testing' },
93
+ { column: 'review' }
94
+ ]);
95
+ assert.strictEqual(progress.tasks_testing, 1);
96
+ assert.strictEqual(progress.tasks_review, 1);
97
+
98
+ console.log('✓ kanban.test.js passed');
99
+ }
100
+
101
+ run().catch((err) => {
102
+ console.error(err);
103
+ process.exit(1);
104
+ });
package/tests/run.js CHANGED
@@ -18,10 +18,16 @@ runNode(path.join('tests', 'update-tasks.test.js'), [], 'Update tasks test');
18
18
  runNode(path.join('tests', 'read-views.test.js'), [], 'Read views test');
19
19
  runNode(path.join('tests', 'mcp-server.test.js'), [], 'MCP server test');
20
20
  runNode(path.join('tests', 'gui-port.test.js'), [], 'GUI port test');
21
+ runNode(path.join('tests', 'gui-cockpit.test.js'), [], 'GUI cockpit layout + workflow test');
21
22
  runNode(path.join('tests', 'fenced-text.test.js'), [], 'Fenced text test');
22
23
  runNode(path.join('tests', 'kanban-mermaid-vendor.test.js'), [], 'Kanban mermaid vendor route test');
23
24
  runNode(path.join('tests', 'plan-workflow.test.js'), [], 'Plan workflow test');
25
+ runNode(path.join('tests', 'workflow-gates.test.js'), [], 'Workflow gates test');
26
+ runNode(path.join('tests', 'kanban.js'), [], 'Kanban core test');
27
+ runNode(path.join('tests', 'index.js'), [], 'Package index export test');
28
+ runNode(path.join('tests', 'bin-kanban.test.js'), [], 'CLI bin/kanban test');
24
29
  runNode(path.join('tests', 'agent-playbook.test.js'), [], 'Agent playbook test');
25
30
  runNode(path.join('tests', 'epics.test.js'), [], 'Epics test');
31
+ runNode(path.join('tests', 'kanban-epic-goals-adr.test.js'), [], 'Epic goals + ADR test');
26
32
  runNode(path.join('tests', 'delete-archive.test.js'), [], 'Delete/archive test');
27
33
  runNode(path.join('tests', 'race-conditions.test.js'), [], 'Race conditions test');