kushi-agents 6.1.2 → 6.2.0

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.
Files changed (86) hide show
  1. package/package.json +1 -1
  2. package/plugin/instructions/agentskills-compliance.instructions.md +144 -0
  3. package/plugin/instructions/dashboard-artifact.instructions.md +132 -0
  4. package/plugin/instructions/guided-tour.instructions.md +100 -0
  5. package/plugin/instructions/karpathy-state-layout.instructions.md +124 -0
  6. package/plugin/instructions/schema-evolve.instructions.md +73 -0
  7. package/plugin/instructions/skill-authoring.instructions.md +147 -0
  8. package/plugin/instructions/skill-evals.instructions.md +130 -0
  9. package/plugin/runners/bootstrap.mjs +55 -22
  10. package/plugin/runners/lib/runlog.mjs +153 -6
  11. package/plugin/runners/migrate-to-v550.mjs +192 -0
  12. package/plugin/runners/pull-email.mjs +194 -3
  13. package/plugin/runners/pull-meetings.mjs +207 -4
  14. package/plugin/runners/pull-onenote.mjs +239 -3
  15. package/plugin/runners/pull-sharepoint.mjs +284 -3
  16. package/plugin/runners/pull-state.mjs +297 -0
  17. package/plugin/runners/pull-teams.mjs +170 -3
  18. package/plugin/runners/refresh.mjs +9 -1
  19. package/plugin/runners/test/fixtures/email-abn-amro.json +13 -0
  20. package/plugin/runners/test/fixtures/email-novel-error.json +9 -0
  21. package/plugin/runners/test/fixtures/meetings-abn-amro.json +10 -0
  22. package/plugin/runners/test/fixtures/meetings-body-unavailable.json +10 -0
  23. package/plugin/runners/test/fixtures/onenote-abn-amro.json +30 -0
  24. package/plugin/runners/test/fixtures/onenote-partial.json +21 -0
  25. package/plugin/runners/test/fixtures/refresh-dir/email.json +7 -4
  26. package/plugin/runners/test/fixtures/refresh-dir/teams.json +6 -4
  27. package/plugin/runners/test/fixtures/sharepoint-abn-amro.json +12 -0
  28. package/plugin/runners/test/fixtures/teams-abn-amro.json +11 -0
  29. package/plugin/runners/test/integration/migrate-to-v550.integration.test.mjs +138 -0
  30. package/plugin/runners/test/integration/pull-email.integration.test.mjs +149 -0
  31. package/plugin/runners/test/integration/pull-meetings.integration.test.mjs +92 -0
  32. package/plugin/runners/test/integration/pull-onenote.integration.test.mjs +86 -0
  33. package/plugin/runners/test/integration/pull-sharepoint.integration.test.mjs +93 -0
  34. package/plugin/runners/test/integration/pull-teams.integration.test.mjs +91 -0
  35. package/plugin/runners/test/unit/runlog.test.mjs +1 -1
  36. package/plugin/skills/build-state/SKILL.md +195 -0
  37. package/plugin/skills/build-state/evals/evals.json +31 -0
  38. package/plugin/skills/dashboard/SKILL.md +132 -0
  39. package/plugin/skills/dashboard/evals/evals.json +33 -0
  40. package/plugin/skills/lint-state/.created-by-skill-creator +0 -0
  41. package/plugin/skills/lint-state/SKILL.md +98 -0
  42. package/plugin/skills/lint-state/evals/evals.json +34 -0
  43. package/plugin/skills/lint-state/lint.ps1 +218 -0
  44. package/plugin/skills/promote/.created-by-skill-creator +1 -0
  45. package/plugin/skills/promote/SKILL.md +125 -0
  46. package/plugin/skills/promote/evals/evals.json +35 -0
  47. package/plugin/skills/schema-evolve/.created-by-skill-creator +0 -0
  48. package/plugin/skills/schema-evolve/SKILL.md +106 -0
  49. package/plugin/skills/schema-evolve/evals/evals.json +37 -0
  50. package/plugin/skills/skill-checker/SKILL.md +136 -0
  51. package/plugin/skills/skill-checker/check-skill.ps1 +416 -0
  52. package/plugin/skills/skill-checker/evals/evals.json +41 -0
  53. package/plugin/skills/skill-creator/SKILL.md +134 -0
  54. package/plugin/skills/skill-creator/evals/evals.json +40 -0
  55. package/plugin/skills/skill-creator/generate-eval-review.ps1 +101 -0
  56. package/plugin/skills/skill-creator/optimize-description.ps1 +87 -0
  57. package/plugin/skills/skill-creator/scaffold.ps1 +180 -0
  58. package/plugin/skills/skill-creator/templates/evals-starter.template.json +27 -0
  59. package/plugin/skills/skill-creator/templates/gotchas-stub.template.md +9 -0
  60. package/plugin/skills/skill-creator/templates/skill-skeleton.template.md +28 -0
  61. package/plugin/skills/teach/.created-by-skill-creator +0 -0
  62. package/plugin/skills/teach/SKILL.md +79 -0
  63. package/plugin/skills/teach/evals/evals.json +59 -0
  64. package/plugin/skills/tour/SKILL.md +85 -0
  65. package/plugin/skills/tour/build-tour.ps1 +185 -0
  66. package/plugin/skills/tour/evals/evals.json +33 -0
  67. package/plugin/templates/state/00_overview.template.md +44 -0
  68. package/plugin/templates/state/01_decisions.template.md +41 -0
  69. package/plugin/templates/state/02_stakeholders.template.md +48 -0
  70. package/plugin/templates/state/03_architecture-and-solution.template.md +56 -0
  71. package/plugin/templates/state/04_workshops-and-key-meetings.template.md +43 -0
  72. package/plugin/templates/state/05_action-items.template.md +29 -0
  73. package/plugin/templates/state/06_risks-and-issues.template.md +43 -0
  74. package/plugin/templates/state/07_timeline-and-milestones.template.md +45 -0
  75. package/plugin/templates/state/08_artifacts-and-deliverables.template.md +55 -0
  76. package/plugin/templates/state/09_open-questions.template.md +62 -0
  77. package/plugin/templates/state/AGENTS.template.md +33 -0
  78. package/plugin/templates/state/CLAUDE.template.md +33 -0
  79. package/plugin/templates/state/README.md +41 -0
  80. package/plugin/templates/state/answers.README.md +7 -0
  81. package/plugin/templates/state/hot.template.md +12 -0
  82. package/plugin/templates/state/index.template.md +41 -0
  83. package/plugin/templates/state/log.template.md +14 -0
  84. package/plugin/templates/state/page.template.md +22 -0
  85. package/plugin/templates/state/review-queue.template.md +10 -0
  86. package/plugin/runners/test/integration/csc-pull.integration.test.mjs +0 -160
@@ -0,0 +1,138 @@
1
+ // plugin/runners/test/integration/migrate-to-v550.integration.test.mjs
2
+
3
+ import { test } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { promises as fs } from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import { spawnSync } from 'node:child_process';
9
+ import YAML from 'yaml';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
13
+ const REPO_ROOT = path.resolve(HERE, '..', '..', '..', '..');
14
+ const RUNNER = path.join(REPO_ROOT, 'plugin', 'runners', 'migrate-to-v550.mjs');
15
+
16
+ async function makeV4Project({ withBoundaries = true } = {}) {
17
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'kushi-migrate-int-'));
18
+ const integrations = {
19
+ crm: { request_id: 'REQ-123' },
20
+ ado: { engagement_id: 100 },
21
+ sharepoint: { sites: ['https://example.sharepoint.com/sites/proj'] },
22
+ };
23
+ if (withBoundaries) {
24
+ // v4.x shape: boundaries: under integrations.yml with per-user fields mixed in.
25
+ integrations.boundaries = {
26
+ crm: { entity_uri: 'crm://...' },
27
+ email: { folders: ['Project Inbox'] },
28
+ teams: { chats: ['19:abc@thread.v2'] },
29
+ meetings: { joinUrls: [] },
30
+ onenote: { section_file_ids: ['sec-1'] },
31
+ };
32
+ }
33
+ await fs.writeFile(path.join(root, 'integrations.yml'), YAML.stringify(integrations));
34
+ return root;
35
+ }
36
+
37
+ function runMigrate(project, alias, extra = []) {
38
+ return spawnSync(process.execPath, [RUNNER,
39
+ '--project', project, '--alias', alias, ...extra,
40
+ ], { encoding: 'utf8' });
41
+ }
42
+
43
+ test('v4.x project: splits boundaries into per-user file and rewrites integrations.yml', async () => {
44
+ const p = await makeV4Project();
45
+ try {
46
+ const res = runMigrate(p, 'ushak');
47
+ assert.equal(res.status, 0, `stderr: ${res.stderr}`);
48
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
49
+ assert.equal(r.status, 'ok');
50
+ assert.equal(r.boundaries_split, true);
51
+ assert.equal(r.integrations_rewritten, true);
52
+ assert.equal(r.unchanged, false);
53
+
54
+ // integrations.yml no longer has boundaries: block; keeps shared keys.
55
+ const intAfter = YAML.parse(await fs.readFile(path.join(p, 'integrations.yml'), 'utf8'));
56
+ assert.equal(intAfter.boundaries, undefined);
57
+ assert.equal(intAfter.crm.request_id, 'REQ-123');
58
+ assert.equal(intAfter.ado.engagement_id, 100);
59
+ assert.deepEqual(intAfter.sharepoint.sites, ['https://example.sharepoint.com/sites/proj']);
60
+
61
+ // per-user boundaries.yml has the moved keys.
62
+ const bAfter = YAML.parse(await fs.readFile(path.join(p, 'Evidence', 'ushak', 'boundaries.yml'), 'utf8'));
63
+ assert.deepEqual(bAfter.email.folders, ['Project Inbox']);
64
+ assert.deepEqual(bAfter.teams.chats, ['19:abc@thread.v2']);
65
+ assert.deepEqual(bAfter.onenote.section_file_ids, ['sec-1']);
66
+ // crm.entity_uri is NOT moved (it's a shared key under boundaries → promoted to top)
67
+ assert.equal(bAfter.crm, undefined);
68
+ } finally { await fs.rm(p, { recursive: true, force: true }); }
69
+ });
70
+
71
+ test('scaffolds _shared/* and per-user dirs', async () => {
72
+ const p = await makeV4Project();
73
+ try {
74
+ runMigrate(p, 'ushak');
75
+ for (const d of ['crm', 'ado', 'sharepoint']) {
76
+ assert.ok((await fs.stat(path.join(p, 'Evidence', '_shared', d))).isDirectory(), `_shared/${d} must exist`);
77
+ }
78
+ for (const d of ['email', 'teams', 'meetings', 'onenote', 'sharepoint', 'crm-notes', 'ado-notes', '_deferred-retries', '_discovery', 'refresh-reports']) {
79
+ assert.ok((await fs.stat(path.join(p, 'Evidence', 'ushak', d))).isDirectory(), `Evidence/ushak/${d} must exist`);
80
+ }
81
+ assert.ok((await fs.stat(path.join(p, 'Evidence', 'ushak', '_ledger.yml'))).isFile());
82
+ } finally { await fs.rm(p, { recursive: true, force: true }); }
83
+ });
84
+
85
+ test('idempotent: second run reports unchanged: true', async () => {
86
+ const p = await makeV4Project();
87
+ try {
88
+ runMigrate(p, 'ushak');
89
+ const r = JSON.parse(runMigrate(p, 'ushak').stdout.trim().split('\n').pop());
90
+ assert.equal(r.unchanged, true);
91
+ assert.deepEqual(r.dirs_created, []);
92
+ assert.deepEqual(r.files_created, []);
93
+ assert.equal(r.boundaries_split, false);
94
+ assert.equal(r.integrations_rewritten, false);
95
+ } finally { await fs.rm(p, { recursive: true, force: true }); }
96
+ });
97
+
98
+ test('--dry-run does not write anything', async () => {
99
+ const p = await makeV4Project();
100
+ try {
101
+ const res = runMigrate(p, 'ushak', ['--dry-run']);
102
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
103
+ assert.equal(r.dry_run, true);
104
+ // Reports planned changes
105
+ assert.ok(r.boundaries_split || r.integrations_rewritten || r.dirs_created.length > 0);
106
+ // But disk is untouched
107
+ assert.equal((await fs.readdir(p)).includes('Evidence'), false, 'Evidence/ must not exist after --dry-run');
108
+ const intAfter = YAML.parse(await fs.readFile(path.join(p, 'integrations.yml'), 'utf8'));
109
+ assert.ok(intAfter.boundaries, 'integrations.yml must be untouched');
110
+ } finally { await fs.rm(p, { recursive: true, force: true }); }
111
+ });
112
+
113
+ test('v5.5.0-shape project (no boundaries: block): only scaffolds missing dirs', async () => {
114
+ const p = await makeV4Project({ withBoundaries: false });
115
+ try {
116
+ const r = JSON.parse(runMigrate(p, 'ushak').stdout.trim().split('\n').pop());
117
+ assert.equal(r.boundaries_split, false);
118
+ assert.equal(r.integrations_rewritten, false);
119
+ // dirs were scaffolded
120
+ assert.ok(r.dirs_created.length > 0);
121
+ // boundaries.yml stub written
122
+ const bAfter = YAML.parse(await fs.readFile(path.join(p, 'Evidence', 'ushak', 'boundaries.yml'), 'utf8'));
123
+ assert.deepEqual(bAfter.email.folders, []);
124
+ } finally { await fs.rm(p, { recursive: true, force: true }); }
125
+ });
126
+
127
+ test('missing --project exits 2', () => {
128
+ const res = spawnSync(process.execPath, [RUNNER], { encoding: 'utf8' });
129
+ assert.equal(res.status, 2);
130
+ });
131
+
132
+ test('non-existent project reports error and exits 1', async () => {
133
+ const res = runMigrate(path.join(os.tmpdir(), 'definitely-not-a-real-project-xyz'), 'ushak');
134
+ assert.equal(res.status, 1);
135
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
136
+ assert.equal(r.status, 'partial');
137
+ assert.equal(r.errors[0].signature, 'project-missing');
138
+ });
@@ -0,0 +1,149 @@
1
+ // plugin/runners/test/integration/pull-email.integration.test.mjs
2
+
3
+ import { test, before, after } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { promises as fs } from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import { spawnSync } from 'node:child_process';
9
+ import YAML from 'yaml';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
13
+ const REPO_ROOT = path.resolve(HERE, '..', '..', '..', '..');
14
+ const RUNNER = path.join(REPO_ROOT, 'plugin', 'runners', 'pull-email.mjs');
15
+ const FIXTURE = path.join(HERE, '..', 'fixtures', 'email-abn-amro.json');
16
+
17
+ let projectRoot;
18
+
19
+ before(async () => {
20
+ projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kushi-email-int-'));
21
+ await fs.mkdir(path.join(projectRoot, 'Evidence', 'ushak'), { recursive: true });
22
+ await fs.writeFile(path.join(projectRoot, 'integrations.yml'), YAML.stringify({}));
23
+ });
24
+ after(async () => { await fs.rm(projectRoot, { recursive: true, force: true }); });
25
+
26
+ function runRunner(extra = []) {
27
+ return spawnSync(process.execPath, [RUNNER,
28
+ '--project', projectRoot, '--alias', 'ushak',
29
+ '--entity', '23. ABN AMRO',
30
+ '--week', '2026-05-25', '--fixture', FIXTURE,
31
+ ...extra,
32
+ ], { encoding: 'utf8' });
33
+ }
34
+
35
+ test('captures only messages within the ISO week window', () => {
36
+ const res = runRunner();
37
+ assert.equal(res.status, 0, `stderr: ${res.stderr}`);
38
+ const result = JSON.parse(res.stdout.trim().split('\n').pop());
39
+ assert.equal(result.status, 'captured');
40
+ assert.equal(result.items_pulled, 2);
41
+ });
42
+
43
+ test('writes per-week directory under sanitized folder name', async () => {
44
+ const dir = path.join(projectRoot, 'Evidence', 'ushak', 'email', '23. ABN AMRO', '2026-05-25');
45
+ const msgs = YAML.parse(await fs.readFile(path.join(dir, 'messages.yml'), 'utf8'));
46
+ assert.equal(msgs.length, 2);
47
+ const md = await fs.readFile(path.join(dir, 'index.md'), 'utf8');
48
+ assert.match(md, /Kickoff agenda/);
49
+ assert.match(md, /Architecture review notes/);
50
+ });
51
+
52
+ test('ledger entry captured with folder metadata', async () => {
53
+ const ledger = YAML.parse(await fs.readFile(path.join(projectRoot, 'Evidence', 'ushak', '_ledger.yml'), 'utf8'));
54
+ const cell = ledger.entries['email::23. ABN AMRO::2026-05-25'];
55
+ assert.ok(cell);
56
+ assert.equal(cell.last_status, 'captured');
57
+ assert.equal(cell.items_pulled, 2);
58
+ assert.equal(cell.folder_id, 'AAMkAGI=');
59
+ });
60
+
61
+ test('folder not found → status failed (exit 0, recorded in ledger)', async () => {
62
+ const res = spawnSync(process.execPath, [RUNNER,
63
+ '--project', projectRoot, '--alias', 'ushak',
64
+ '--entity', 'NoSuchFolder', '--week', '2026-05-25', '--fixture', FIXTURE,
65
+ ], { encoding: 'utf8' });
66
+ assert.equal(res.status, 0);
67
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
68
+ assert.equal(r.status, 'failed');
69
+ assert.equal(r.errors[0].signature, 'folder-not-found');
70
+ });
71
+
72
+ test('dry-run does not write files', async () => {
73
+ const projectRoot2 = await fs.mkdtemp(path.join(os.tmpdir(), 'kushi-email-dry-'));
74
+ await fs.mkdir(path.join(projectRoot2, 'Evidence', 'ushak'), { recursive: true });
75
+ await fs.writeFile(path.join(projectRoot2, 'integrations.yml'), YAML.stringify({}));
76
+ try {
77
+ const res = spawnSync(process.execPath, [RUNNER,
78
+ '--project', projectRoot2, '--alias', 'ushak',
79
+ '--entity', '23. ABN AMRO', '--week', '2026-05-25', '--fixture', FIXTURE, '--dry-run',
80
+ ], { encoding: 'utf8' });
81
+ assert.equal(res.status, 0);
82
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
83
+ assert.deepEqual(r.files_written, []);
84
+ let exists = true;
85
+ try { await fs.access(path.join(projectRoot2, 'Evidence', 'ushak', 'email')); } catch { exists = false; }
86
+ assert.equal(exists, false);
87
+ } finally {
88
+ await fs.rm(projectRoot2, { recursive: true, force: true });
89
+ }
90
+ });
91
+
92
+ test('missing --entity exits 2', () => {
93
+ const res = spawnSync(process.execPath, [RUNNER,
94
+ '--project', projectRoot, '--alias', 'ushak', '--fixture', FIXTURE,
95
+ ], { encoding: 'utf8' });
96
+ assert.equal(res.status, 2);
97
+ });
98
+
99
+ test('v5.6.0: non-retryable error emits a learning candidate file', async () => {
100
+ const projectRoot3 = await fs.mkdtemp(path.join(os.tmpdir(), 'kushi-email-novel-'));
101
+ await fs.mkdir(path.join(projectRoot3, 'Evidence', 'ushak'), { recursive: true });
102
+ await fs.writeFile(path.join(projectRoot3, 'integrations.yml'), YAML.stringify({}));
103
+ const NOVEL_FIXTURE = path.join(HERE, '..', 'fixtures', 'email-novel-error.json');
104
+ try {
105
+ const res = spawnSync(process.execPath, [RUNNER,
106
+ '--project', projectRoot3, '--alias', 'ushak',
107
+ '--entity', '23. ABN AMRO', '--week', '2026-05-25', '--fixture', NOVEL_FIXTURE,
108
+ ], { encoding: 'utf8' });
109
+ assert.equal(res.status, 0, `stderr: ${res.stderr}`);
110
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
111
+ assert.equal(r.status, 'failed');
112
+ const candDir = path.join(projectRoot3, 'Evidence', '_learnings-candidates');
113
+ const entries = await fs.readdir(candDir);
114
+ const mdFiles = entries.filter(f => f.endsWith('.md'));
115
+ assert.equal(mdFiles.length, 1, `expected exactly one candidate, got: ${entries.join(', ')}`);
116
+ const body = await fs.readFile(path.join(candDir, mdFiles[0]), 'utf8');
117
+ assert.match(body, /fetch-failed/);
118
+ assert.match(body, /unexpected null/);
119
+ // Re-run within window — should not duplicate.
120
+ spawnSync(process.execPath, [RUNNER,
121
+ '--project', projectRoot3, '--alias', 'ushak',
122
+ '--entity', '23. ABN AMRO', '--week', '2026-05-25', '--fixture', NOVEL_FIXTURE,
123
+ ], { encoding: 'utf8' });
124
+ const entries2 = await fs.readdir(candDir);
125
+ const mdFiles2 = entries2.filter(f => f.endsWith('.md'));
126
+ assert.equal(mdFiles2.length, 1, 'dedup should prevent a 2nd file within 7d');
127
+ } finally {
128
+ await fs.rm(projectRoot3, { recursive: true, force: true });
129
+ }
130
+ });
131
+
132
+ test('v5.6.0: user-side error does NOT emit a learning candidate', async () => {
133
+ const projectRoot4 = await fs.mkdtemp(path.join(os.tmpdir(), 'kushi-email-userside-'));
134
+ await fs.mkdir(path.join(projectRoot4, 'Evidence', 'ushak'), { recursive: true });
135
+ await fs.writeFile(path.join(projectRoot4, 'integrations.yml'), YAML.stringify({}));
136
+ try {
137
+ const res = spawnSync(process.execPath, [RUNNER,
138
+ '--project', projectRoot4, '--alias', 'ushak',
139
+ '--entity', 'NoSuchFolder', '--week', '2026-05-25', '--fixture', FIXTURE,
140
+ ], { encoding: 'utf8' });
141
+ assert.equal(res.status, 0);
142
+ let exists = true;
143
+ try { await fs.access(path.join(projectRoot4, 'Evidence', '_learnings-candidates')); }
144
+ catch { exists = false; }
145
+ assert.equal(exists, false, 'folder-not-found should never trigger candidate emission');
146
+ } finally {
147
+ await fs.rm(projectRoot4, { recursive: true, force: true });
148
+ }
149
+ });
@@ -0,0 +1,92 @@
1
+ // plugin/runners/test/integration/pull-meetings.integration.test.mjs
2
+
3
+ import { test, before, after } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { promises as fs } from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import { spawnSync } from 'node:child_process';
9
+ import YAML from 'yaml';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
13
+ const REPO_ROOT = path.resolve(HERE, '..', '..', '..', '..');
14
+ const RUNNER = path.join(REPO_ROOT, 'plugin', 'runners', 'pull-meetings.mjs');
15
+ const FIXTURE_OK = path.join(HERE, '..', 'fixtures', 'meetings-abn-amro.json');
16
+ const FIXTURE_BU = path.join(HERE, '..', 'fixtures', 'meetings-body-unavailable.json');
17
+ const JOIN_URL_OK = 'https://teams.microsoft.com/meet/31827546911768?p=YOgZHuaffeF0MGBCNt';
18
+ const JOIN_URL_BU = 'https://teams.microsoft.com/meet/26457824867211?p=zdSjg6cqHoHPA8Hx11';
19
+
20
+ let projectRoot;
21
+
22
+ before(async () => {
23
+ projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kushi-meet-int-'));
24
+ await fs.mkdir(path.join(projectRoot, 'Evidence', 'ushak'), { recursive: true });
25
+ await fs.writeFile(path.join(projectRoot, 'integrations.yml'), YAML.stringify({}));
26
+ });
27
+ after(async () => { await fs.rm(projectRoot, { recursive: true, force: true }); });
28
+
29
+ test('captures meeting + transcript verbatim', () => {
30
+ const res = spawnSync(process.execPath, [RUNNER,
31
+ '--project', projectRoot, '--alias', 'ushak',
32
+ '--entity', JOIN_URL_OK, '--week', '2026-05-25', '--fixture', FIXTURE_OK,
33
+ ], { encoding: 'utf8' });
34
+ assert.equal(res.status, 0, `stderr: ${res.stderr}`);
35
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
36
+ assert.equal(r.status, 'captured');
37
+ assert.equal(r.items_pulled, 1);
38
+ assert.ok(r.transcript_chars > 0);
39
+ });
40
+
41
+ test('transcript written verbatim to verbatim/transcript.txt', async () => {
42
+ // Find the meetings directory (only one)
43
+ const meetDir = path.join(projectRoot, 'Evidence', 'ushak', 'meetings');
44
+ const entries = await fs.readdir(meetDir);
45
+ assert.equal(entries.length, 1);
46
+ const tx = await fs.readFile(path.join(meetDir, entries[0], 'verbatim', 'transcript.txt'), 'utf8');
47
+ assert.match(tx, /WEBVTT/);
48
+ assert.match(tx, /Architecture diagram is ready for review/);
49
+ });
50
+
51
+ test('body-unavailable → defers (4h marker) and writes meeting.yml only', async () => {
52
+ const projectRoot2 = await fs.mkdtemp(path.join(os.tmpdir(), 'kushi-meet-bu-'));
53
+ await fs.mkdir(path.join(projectRoot2, 'Evidence', 'ushak'), { recursive: true });
54
+ await fs.writeFile(path.join(projectRoot2, 'integrations.yml'), YAML.stringify({}));
55
+ try {
56
+ const res = spawnSync(process.execPath, [RUNNER,
57
+ '--project', projectRoot2, '--alias', 'ushak',
58
+ '--entity', JOIN_URL_BU, '--week', '2026-05-25', '--fixture', FIXTURE_BU,
59
+ ], { encoding: 'utf8' });
60
+ assert.equal(res.status, 0, `stderr: ${res.stderr}`);
61
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
62
+ assert.equal(r.status, 'body-unavailable');
63
+ assert.equal(r.errors[0].signature, 'body-unavailable');
64
+ assert.equal(r.errors[0].retry_after_min, 240);
65
+ // deferred-retries directory has an entry
66
+ const deferredDir = path.join(projectRoot2, 'Evidence', 'ushak', '_deferred-retries', 'meetings');
67
+ const files = await fs.readdir(deferredDir);
68
+ assert.ok(files.length >= 1);
69
+ // No transcript file written
70
+ const meetDir = path.join(projectRoot2, 'Evidence', 'ushak', 'meetings');
71
+ const entries = await fs.readdir(meetDir);
72
+ let txExists = true;
73
+ try { await fs.access(path.join(meetDir, entries[0], 'verbatim', 'transcript.txt')); } catch { txExists = false; }
74
+ assert.equal(txExists, false);
75
+ } finally { await fs.rm(projectRoot2, { recursive: true, force: true }); }
76
+ });
77
+
78
+ test('ledger reflects captured state with transcript_chars', async () => {
79
+ const ledger = YAML.parse(await fs.readFile(path.join(projectRoot, 'Evidence', 'ushak', '_ledger.yml'), 'utf8'));
80
+ const cell = ledger.entries[`meetings::${JOIN_URL_OK}::2026-05-25`];
81
+ assert.ok(cell);
82
+ assert.equal(cell.last_status, 'captured');
83
+ assert.equal(cell.transcript_available, true);
84
+ assert.ok(cell.transcript_chars > 0);
85
+ });
86
+
87
+ test('missing --entity exits 2', () => {
88
+ const res = spawnSync(process.execPath, [RUNNER,
89
+ '--project', projectRoot, '--alias', 'ushak', '--fixture', FIXTURE_OK,
90
+ ], { encoding: 'utf8' });
91
+ assert.equal(res.status, 2);
92
+ });
@@ -0,0 +1,86 @@
1
+ // plugin/runners/test/integration/pull-onenote.integration.test.mjs
2
+
3
+ import { test, before, after } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { promises as fs } from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import { spawnSync } from 'node:child_process';
9
+ import YAML from 'yaml';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
13
+ const REPO_ROOT = path.resolve(HERE, '..', '..', '..', '..');
14
+ const RUNNER = path.join(REPO_ROOT, 'plugin', 'runners', 'pull-onenote.mjs');
15
+ const FIXTURE_OK = path.join(HERE, '..', 'fixtures', 'onenote-abn-amro.json');
16
+ const FIXTURE_PARTIAL = path.join(HERE, '..', 'fixtures', 'onenote-partial.json');
17
+ const SECTION_ID = '015a0ac9-58f4-4f34-98b3-a60075b36627';
18
+
19
+ let projectRoot;
20
+
21
+ before(async () => {
22
+ projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kushi-on-int-'));
23
+ await fs.mkdir(path.join(projectRoot, 'Evidence', 'ushak'), { recursive: true });
24
+ await fs.writeFile(path.join(projectRoot, 'integrations.yml'), YAML.stringify({}));
25
+ });
26
+ after(async () => { await fs.rm(projectRoot, { recursive: true, force: true }); });
27
+
28
+ test('captures all pages within the week, writes one HTML file per page', () => {
29
+ const res = spawnSync(process.execPath, [RUNNER,
30
+ '--project', projectRoot, '--alias', 'ushak',
31
+ '--entity', SECTION_ID, '--week', '2026-05-25', '--fixture', FIXTURE_OK,
32
+ ], { encoding: 'utf8' });
33
+ assert.equal(res.status, 0, `stderr: ${res.stderr}`);
34
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
35
+ assert.equal(r.status, 'captured');
36
+ assert.equal(r.pages_enumerated, 2); // p1 + p2 (p3 is stale)
37
+ assert.equal(r.items_pulled, 2);
38
+ });
39
+
40
+ test('per-page HTML written verbatim under content/<id>.html', async () => {
41
+ const dir = path.join(projectRoot, 'Evidence', 'ushak', 'onenote', SECTION_ID, '2026-05-25', 'content');
42
+ const p1 = await fs.readFile(path.join(dir, 'p1.html'), 'utf8');
43
+ const p2 = await fs.readFile(path.join(dir, 'p2.html'), 'utf8');
44
+ assert.match(p1, /Kickoff/);
45
+ assert.match(p2, /Three-tier/);
46
+ });
47
+
48
+ test('partial: body-unavailable pages NOT enqueued in deferred-retries', async () => {
49
+ const projectRoot2 = await fs.mkdtemp(path.join(os.tmpdir(), 'kushi-on-part-'));
50
+ await fs.mkdir(path.join(projectRoot2, 'Evidence', 'ushak'), { recursive: true });
51
+ await fs.writeFile(path.join(projectRoot2, 'integrations.yml'), YAML.stringify({}));
52
+ try {
53
+ const res = spawnSync(process.execPath, [RUNNER,
54
+ '--project', projectRoot2, '--alias', 'ushak',
55
+ '--entity', SECTION_ID, '--week', '2026-05-25', '--fixture', FIXTURE_PARTIAL,
56
+ ], { encoding: 'utf8' });
57
+ assert.equal(res.status, 0, `stderr: ${res.stderr}`);
58
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
59
+ assert.equal(r.status, 'partial');
60
+ assert.equal(r.items_pulled, 1);
61
+ assert.deepEqual(r.body_unavailable, ['p2']);
62
+ // Confirm OneNote is forbidden from enqueueing body-unavailable (lib/deferred.mjs throws)
63
+ let deferredCount = 0;
64
+ try {
65
+ const files = await fs.readdir(path.join(projectRoot2, 'Evidence', 'ushak', '_deferred-retries', 'onenote'));
66
+ deferredCount = files.length;
67
+ } catch { deferredCount = 0; }
68
+ assert.equal(deferredCount, 0);
69
+ } finally { await fs.rm(projectRoot2, { recursive: true, force: true }); }
70
+ });
71
+
72
+ test('ledger captures pages_enumerated and section_file_id', async () => {
73
+ const ledger = YAML.parse(await fs.readFile(path.join(projectRoot, 'Evidence', 'ushak', '_ledger.yml'), 'utf8'));
74
+ const cell = ledger.entries[`onenote::${SECTION_ID}::2026-05-25`];
75
+ assert.ok(cell);
76
+ assert.equal(cell.last_status, 'captured');
77
+ assert.equal(cell.pages_enumerated, 2);
78
+ assert.equal(cell.section_file_id, SECTION_ID);
79
+ });
80
+
81
+ test('missing --entity exits 2', () => {
82
+ const res = spawnSync(process.execPath, [RUNNER,
83
+ '--project', projectRoot, '--alias', 'ushak', '--fixture', FIXTURE_OK,
84
+ ], { encoding: 'utf8' });
85
+ assert.equal(res.status, 2);
86
+ });
@@ -0,0 +1,93 @@
1
+ // plugin/runners/test/integration/pull-sharepoint.integration.test.mjs
2
+
3
+ import { test, before, after } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { promises as fs } from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import { spawnSync } from 'node:child_process';
9
+ import YAML from 'yaml';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
13
+ const REPO_ROOT = path.resolve(HERE, '..', '..', '..', '..');
14
+ const RUNNER = path.join(REPO_ROOT, 'plugin', 'runners', 'pull-sharepoint.mjs');
15
+ const FIXTURE = path.join(HERE, '..', 'fixtures', 'sharepoint-abn-amro.json');
16
+ const SITE_OK = 'https://abnamro.sharepoint.com/sites/TeamAlfred';
17
+ const SITE_BAD = 'https://contoso-evil.sharepoint.com/sites/X';
18
+
19
+ let projectRoot;
20
+
21
+ before(async () => {
22
+ projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kushi-sp-int-'));
23
+ await fs.mkdir(path.join(projectRoot, 'Evidence', 'ushak'), { recursive: true });
24
+ await fs.writeFile(path.join(projectRoot, 'integrations.yml'), YAML.stringify({}));
25
+ });
26
+ after(async () => { await fs.rm(projectRoot, { recursive: true, force: true }); });
27
+
28
+ test('captures items within ISO week — no bundling (one file per item)', async () => {
29
+ const res = spawnSync(process.execPath, [RUNNER,
30
+ '--project', projectRoot, '--alias', 'ushak',
31
+ '--entity', SITE_OK, '--week', '2026-05-25',
32
+ '--allowed-tenants', 'abnamro.sharepoint.com',
33
+ '--fixture', FIXTURE,
34
+ ], { encoding: 'utf8' });
35
+ assert.equal(res.status, 0, `stderr: ${res.stderr}`);
36
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
37
+ assert.equal(r.status, 'captured');
38
+ assert.equal(r.items_pulled, 2);
39
+ // No bundle file like items.yml — only one-file-per-item under items/
40
+ const dir = path.join(projectRoot, 'Evidence', '_shared', 'sharepoint', r.site_hash, '2026-05-25');
41
+ const itemFiles = await fs.readdir(path.join(dir, 'items'));
42
+ assert.equal(itemFiles.length, 2);
43
+ for (const f of itemFiles) assert.ok(f.endsWith('.yml'));
44
+ });
45
+
46
+ test('cross-tenant site blocked → status failed', async () => {
47
+ const projectRoot2 = await fs.mkdtemp(path.join(os.tmpdir(), 'kushi-sp-xtenant-'));
48
+ await fs.mkdir(path.join(projectRoot2, 'Evidence', 'ushak'), { recursive: true });
49
+ await fs.writeFile(path.join(projectRoot2, 'integrations.yml'), YAML.stringify({}));
50
+ try {
51
+ const res = spawnSync(process.execPath, [RUNNER,
52
+ '--project', projectRoot2, '--alias', 'ushak',
53
+ '--entity', SITE_BAD, '--week', '2026-05-25',
54
+ '--allowed-tenants', 'abnamro.sharepoint.com,microsoft.sharepoint.com',
55
+ '--fixture', FIXTURE,
56
+ ], { encoding: 'utf8' });
57
+ assert.equal(res.status, 0);
58
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
59
+ assert.equal(r.status, 'failed');
60
+ assert.equal(r.errors[0].signature, 'cross-tenant-blocked');
61
+ // No items written
62
+ let exists = true;
63
+ try { await fs.access(path.join(projectRoot2, 'Evidence', '_shared', 'sharepoint')); } catch { exists = false; }
64
+ assert.equal(exists, false);
65
+ } finally { await fs.rm(projectRoot2, { recursive: true, force: true }); }
66
+ });
67
+
68
+ test('item files have correct content', async () => {
69
+ const r = JSON.parse(spawnSync(process.execPath, [RUNNER,
70
+ '--project', projectRoot, '--alias', 'ushak',
71
+ '--entity', SITE_OK, '--week', '2026-05-25',
72
+ '--allowed-tenants', 'abnamro.sharepoint.com',
73
+ '--fixture', FIXTURE,
74
+ ], { encoding: 'utf8' }).stdout.trim().split('\n').pop());
75
+ const dir = path.join(projectRoot, 'Evidence', '_shared', 'sharepoint', r.site_hash, '2026-05-25', 'items');
76
+ const it = YAML.parse(await fs.readFile(path.join(dir, '01ABC.yml'), 'utf8'));
77
+ assert.equal(it.name, 'Architecture.pptx');
78
+ });
79
+
80
+ test('ledger entry uses raw site url as cell key', async () => {
81
+ const ledger = YAML.parse(await fs.readFile(path.join(projectRoot, 'Evidence', 'ushak', '_ledger.yml'), 'utf8'));
82
+ const cell = ledger.entries[`sharepoint::${SITE_OK}::2026-05-25`];
83
+ assert.ok(cell);
84
+ assert.equal(cell.last_status, 'captured');
85
+ assert.equal(cell.items_pulled, 2);
86
+ });
87
+
88
+ test('missing --entity exits 2', () => {
89
+ const res = spawnSync(process.execPath, [RUNNER,
90
+ '--project', projectRoot, '--alias', 'ushak', '--fixture', FIXTURE,
91
+ ], { encoding: 'utf8' });
92
+ assert.equal(res.status, 2);
93
+ });
@@ -0,0 +1,91 @@
1
+ // plugin/runners/test/integration/pull-teams.integration.test.mjs
2
+
3
+ import { test, before, after } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { promises as fs } from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import { spawnSync } from 'node:child_process';
9
+ import YAML from 'yaml';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
13
+ const REPO_ROOT = path.resolve(HERE, '..', '..', '..', '..');
14
+ const RUNNER = path.join(REPO_ROOT, 'plugin', 'runners', 'pull-teams.mjs');
15
+ const FIXTURE = path.join(HERE, '..', 'fixtures', 'teams-abn-amro.json');
16
+ const CHAT_ID = '19:e17bf64ba941442590dae0824cc4784c@thread.v2';
17
+
18
+ let projectRoot;
19
+
20
+ before(async () => {
21
+ projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kushi-teams-int-'));
22
+ await fs.mkdir(path.join(projectRoot, 'Evidence', 'ushak'), { recursive: true });
23
+ await fs.writeFile(path.join(projectRoot, 'integrations.yml'), YAML.stringify({}));
24
+ });
25
+ after(async () => { await fs.rm(projectRoot, { recursive: true, force: true }); });
26
+
27
+ function runRunner(entity, extra = []) {
28
+ return spawnSync(process.execPath, [RUNNER,
29
+ '--project', projectRoot, '--alias', 'ushak',
30
+ '--entity', entity,
31
+ '--week', '2026-05-25', '--fixture', FIXTURE,
32
+ ...extra,
33
+ ], { encoding: 'utf8' });
34
+ }
35
+
36
+ test('captures only messages within the ISO week window', () => {
37
+ const res = runRunner(CHAT_ID);
38
+ assert.equal(res.status, 0, `stderr: ${res.stderr}`);
39
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
40
+ assert.equal(r.status, 'captured');
41
+ assert.equal(r.items_pulled, 3);
42
+ assert.ok(r.chat_hash && r.chat_hash.length === 12);
43
+ });
44
+
45
+ test('uses shortHash for directory (unsafe chat id chars sanitized)', async () => {
46
+ const r = JSON.parse(runRunner(CHAT_ID).stdout.trim().split('\n').pop());
47
+ const dir = path.join(projectRoot, 'Evidence', 'ushak', 'teams', r.chat_hash, '2026-05-25');
48
+ const msgs = YAML.parse(await fs.readFile(path.join(dir, 'messages.yml'), 'utf8'));
49
+ assert.equal(msgs.length, 3);
50
+ const chat = YAML.parse(await fs.readFile(path.join(dir, 'chat.yml'), 'utf8'));
51
+ assert.equal(chat.chat_id, CHAT_ID);
52
+ });
53
+
54
+ test('ledger entry retains raw chat id as cell key', async () => {
55
+ const ledger = YAML.parse(await fs.readFile(path.join(projectRoot, 'Evidence', 'ushak', '_ledger.yml'), 'utf8'));
56
+ const cell = ledger.entries[`teams::${CHAT_ID}::2026-05-25`];
57
+ assert.ok(cell);
58
+ assert.equal(cell.last_status, 'captured');
59
+ assert.equal(cell.items_pulled, 3);
60
+ });
61
+
62
+ test('empty chat returns no-activity', () => {
63
+ const res = runRunner('19:empty@thread.v2');
64
+ assert.equal(res.status, 0);
65
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
66
+ assert.equal(r.status, 'no-activity');
67
+ assert.equal(r.items_pulled, 0);
68
+ assert.deepEqual(r.files_written, []);
69
+ });
70
+
71
+ test('index.md preview strips HTML', async () => {
72
+ const r = JSON.parse(runRunner(CHAT_ID).stdout.trim().split('\n').pop());
73
+ const md = await fs.readFile(path.join(projectRoot, 'Evidence', 'ushak', 'teams', r.chat_hash, '2026-05-25', 'index.md'), 'utf8');
74
+ assert.match(md, /Status update/);
75
+ assert.doesNotMatch(md, /<p>/);
76
+ });
77
+
78
+ test('dry-run writes nothing', async () => {
79
+ const projectRoot2 = await fs.mkdtemp(path.join(os.tmpdir(), 'kushi-teams-dry-'));
80
+ await fs.mkdir(path.join(projectRoot2, 'Evidence', 'ushak'), { recursive: true });
81
+ await fs.writeFile(path.join(projectRoot2, 'integrations.yml'), YAML.stringify({}));
82
+ try {
83
+ const res = spawnSync(process.execPath, [RUNNER,
84
+ '--project', projectRoot2, '--alias', 'ushak',
85
+ '--entity', CHAT_ID, '--week', '2026-05-25', '--fixture', FIXTURE, '--dry-run',
86
+ ], { encoding: 'utf8' });
87
+ assert.equal(res.status, 0);
88
+ const r = JSON.parse(res.stdout.trim().split('\n').pop());
89
+ assert.deepEqual(r.files_written, []);
90
+ } finally { await fs.rm(projectRoot2, { recursive: true, force: true }); }
91
+ });
@@ -45,7 +45,7 @@ test('writeRefreshReport writes timestamped markdown file', async () => {
45
45
  details: { sources: ['email'], count: 1 },
46
46
  });
47
47
  const content = await fs.readFile(r.path, 'utf8');
48
- assert.match(content, /^# refresh —/);
48
+ assert.match(content, /^# Refresh Report —/);
49
49
  assert.match(content, /all good/);
50
50
  assert.match(content, /sources:/);
51
51
  });