@vaultgradient/pipequery-cli 0.6.0 → 0.6.1

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,125 @@
1
+ /**
2
+ * Regression test for the v0.6.0 watches-loader bug:
3
+ *
4
+ * loadConfig() built the typed PipeQueryConfig without copying raw.watches,
5
+ * so config.watches was always undefined. That silently broke three things:
6
+ * 1. pq watch list always reported "no watches"
7
+ * 2. pq watch add overwrote previous watches (load → empty → save → only the new one)
8
+ * 3. WatchManager never started in pq serve (the `if (config.watches && ...)` was always false)
9
+ *
10
+ * The fix is one line in loader.ts. These tests cover the round-trip and the
11
+ * additive save+reload behaviour so the regression can't sneak back.
12
+ */
13
+ import { mkdtemp, rm } from 'node:fs/promises';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
17
+ import { writeYaml } from '../src/utils/yaml.js';
18
+ import { loadConfig, saveConfig } from '../src/config/loader.js';
19
+ import type { PipeQueryConfig, WatchConfig } from '../src/config/schema.js';
20
+
21
+ describe('config loader — watches', () => {
22
+ let dir: string;
23
+ beforeEach(async () => {
24
+ dir = await mkdtemp(join(tmpdir(), 'pq-loader-test-'));
25
+ });
26
+ afterEach(async () => {
27
+ await rm(dir, { recursive: true, force: true });
28
+ });
29
+
30
+ it('preserves watches block from yaml on load', async () => {
31
+ const yamlPath = join(dir, 'pipequery.yaml');
32
+ const watch: WatchConfig = {
33
+ query: 'crypto | first(1)',
34
+ interval: '30s',
35
+ fireWhen: 'on_change',
36
+ notify: { telegram: { chatId: 123456789 } },
37
+ };
38
+ await writeYaml(yamlPath, {
39
+ sources: {},
40
+ endpoints: {},
41
+ watches: { foo: watch },
42
+ });
43
+
44
+ const { config } = await loadConfig(dir);
45
+
46
+ expect(config.watches).toBeDefined();
47
+ expect(Object.keys(config.watches!)).toEqual(['foo']);
48
+ expect(config.watches!.foo.query).toBe('crypto | first(1)');
49
+ expect(config.watches!.foo.fireWhen).toBe('on_change');
50
+ expect(config.watches!.foo.notify.telegram?.chatId).toBe(123456789);
51
+ });
52
+
53
+ it('save+reload keeps existing watches when adding a new one (no clobber)', async () => {
54
+ // Mirrors the user-reported reproduction: two consecutive `pq watch add`
55
+ // invocations should both end up in the yaml.
56
+ const yamlPath = join(dir, 'pipequery.yaml');
57
+ await writeYaml(yamlPath, {
58
+ sources: {},
59
+ endpoints: {},
60
+ });
61
+
62
+ // First add — load (empty), append `foo`, save.
63
+ const first = await loadConfig(dir);
64
+ first.config.watches = first.config.watches ?? {};
65
+ first.config.watches.foo = {
66
+ query: 'a | first(1)',
67
+ notify: { telegram: { chatId: 1 } },
68
+ };
69
+ await saveConfig(first.config, first.path);
70
+
71
+ // Second add — load (must include foo!), append `bar`, save.
72
+ const second = await loadConfig(dir);
73
+ second.config.watches = second.config.watches ?? {};
74
+ second.config.watches.bar = {
75
+ query: 'b | first(1)',
76
+ notify: { telegram: { chatId: 2 } },
77
+ };
78
+ await saveConfig(second.config, second.path);
79
+
80
+ // Reload and verify both watches survived.
81
+ const final = await loadConfig(dir);
82
+ expect(Object.keys(final.config.watches ?? {}).sort()).toEqual(['bar', 'foo']);
83
+ });
84
+
85
+ it('returns an empty watches object when the yaml has no watches block', async () => {
86
+ // Defaulting to {} (rather than undefined) keeps the call sites in
87
+ // watch.ts simple — they can write `config.watches[name] = ...` without
88
+ // a nullish-fallback dance.
89
+ const yamlPath = join(dir, 'pipequery.yaml');
90
+ await writeYaml(yamlPath, {
91
+ sources: {},
92
+ endpoints: {},
93
+ });
94
+
95
+ const { config } = await loadConfig(dir);
96
+ expect(config.watches).toEqual({});
97
+ });
98
+
99
+ it('round-trips a watches block through save → reload unchanged', async () => {
100
+ const yamlPath = join(dir, 'pipequery.yaml');
101
+ const original: PipeQueryConfig = {
102
+ server: { port: 3000, host: '127.0.0.1' },
103
+ sources: {},
104
+ endpoints: {},
105
+ dashboards: {},
106
+ watches: {
107
+ big: {
108
+ query: 'events | where(severity == "high")',
109
+ interval: '60s',
110
+ fireWhen: 'when_non_empty',
111
+ notify: {
112
+ telegram: {
113
+ chatId: -1001234567890,
114
+ message: '🚨 high severity: {{ .msg }}',
115
+ },
116
+ },
117
+ },
118
+ },
119
+ };
120
+ await saveConfig(original, yamlPath);
121
+
122
+ const { config } = await loadConfig(dir);
123
+ expect(config.watches).toEqual(original.watches);
124
+ });
125
+ });
package/dist/index.js CHANGED
@@ -123,7 +123,8 @@ async function loadConfig(cwd = process.cwd()) {
123
123
  remote: raw.remote,
124
124
  sources: raw.sources ?? {},
125
125
  endpoints: raw.endpoints ?? {},
126
- dashboards: raw.dashboards ?? {}
126
+ dashboards: raw.dashboards ?? {},
127
+ watches: raw.watches ?? {}
127
128
  };
128
129
  return { config, path: configPath };
129
130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaultgradient/pipequery-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "CLI for PipeQuery — data sources, aggregations, APIs, and terminal dashboards",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,7 @@ export async function loadConfig(cwd = process.cwd()): Promise<{ config: PipeQue
27
27
  sources: raw.sources ?? {},
28
28
  endpoints: raw.endpoints ?? {},
29
29
  dashboards: raw.dashboards ?? {},
30
+ watches: raw.watches ?? {},
30
31
  };
31
32
 
32
33
  return { config, path: configPath };