@indigoai-us/hq-cli 5.16.0 → 5.17.0-sources-rc.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.
- package/.github/workflows/ci.yml +8 -4
- package/.github/workflows/publish.yml +8 -3
- package/dist/cli-version.d.ts +1 -0
- package/dist/cli-version.js +3 -2
- package/dist/commands/groups.js +6 -6
- package/dist/commands/meetings.js +8 -8
- package/dist/commands/members.d.ts +4 -2
- package/dist/commands/members.js +30 -14
- package/dist/commands/signals.d.ts +24 -0
- package/dist/commands/signals.js +240 -0
- package/dist/commands/sources.d.ts +22 -0
- package/dist/commands/sources.js +250 -0
- package/dist/index.js +8 -2
- package/dist/utils/cognito-session.d.ts +10 -1
- package/dist/utils/cognito-session.js +18 -3
- package/package.json +5 -4
- package/scripts/smoke-sources-signals.sh +103 -0
- package/src/cli-version.ts +5 -1
- package/src/commands/groups.ts +4 -4
- package/src/commands/meetings.ts +6 -6
- package/src/commands/members.test.ts +56 -0
- package/src/commands/members.ts +44 -13
- package/src/commands/signals.ts +345 -0
- package/src/commands/sources.ts +356 -0
- package/src/index.ts +8 -0
- package/src/utils/cognito-session.test.ts +24 -1
- package/src/utils/cognito-session.ts +18 -0
- package/test/commands/signals.test.ts +200 -0
- package/test/commands/sources.test.ts +225 -0
- package/test/fixtures/signals/action_item/sample.md +16 -0
- package/test/fixtures/signals/summary/sample.md +12 -0
- package/test/fixtures/sources/meetings/sample.md +25 -0
- package/test/helpers/cli-runner.ts +150 -0
- package/test/helpers/s3-list-mock.ts +79 -0
- package/test/helpers/vault-service-mock.ts +160 -0
- package/test/sources-signals/smoke.test.ts +226 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smoke.test.ts — Phase 0 infrastructure smoke test (US-001).
|
|
3
|
+
*
|
|
4
|
+
* Validates that:
|
|
5
|
+
* 1. The fixture files exist and have the expected frontmatter shape.
|
|
6
|
+
* 2. mockS3WithEntries lists and retrieves entries with full round-trip fidelity.
|
|
7
|
+
* 3. mockVaultService intercepts /membership/me and /entities/{uid}/sts correctly.
|
|
8
|
+
* 4. runCli(['hq', '--help']) exits 0 (basic wiring check).
|
|
9
|
+
*
|
|
10
|
+
* Note: assertions about 'sources'/'signals' appearing in `hq --help` are
|
|
11
|
+
* intentionally absent here — those land when US-006/US-007 register the
|
|
12
|
+
* commands and add them to buildProgram() in cli-runner.ts.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "fs";
|
|
16
|
+
import { dirname, join } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
19
|
+
import { GetObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
|
|
20
|
+
import { mockS3WithEntries } from "../helpers/s3-list-mock.js";
|
|
21
|
+
import { mockVaultService } from "../helpers/vault-service-mock.js";
|
|
22
|
+
import { runCli } from "../helpers/cli-runner.js";
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
const FIXTURES = join(__dirname, "../fixtures");
|
|
27
|
+
const SOURCE_PATH = join(FIXTURES, "sources/meetings/sample.md");
|
|
28
|
+
const SIGNAL_ACTION_PATH = join(FIXTURES, "signals/action_item/sample.md");
|
|
29
|
+
const SIGNAL_SUMMARY_PATH = join(FIXTURES, "signals/summary/sample.md");
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Fixture shape tests
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
describe("fixtures — file shape", () => {
|
|
36
|
+
it("source fixture has expected frontmatter fields", () => {
|
|
37
|
+
const content = readFileSync(SOURCE_PATH, "utf-8");
|
|
38
|
+
expect(content).toContain("source_id:");
|
|
39
|
+
expect(content).toContain("channel: meeting");
|
|
40
|
+
expect(content).toContain("participants:");
|
|
41
|
+
expect(content).toContain("created_at:");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("action_item signal fixture has required signal fields", () => {
|
|
45
|
+
const content = readFileSync(SIGNAL_ACTION_PATH, "utf-8");
|
|
46
|
+
expect(content).toContain("signal_id:");
|
|
47
|
+
expect(content).toContain("signal_type: action_item");
|
|
48
|
+
expect(content).toContain("source_ref:");
|
|
49
|
+
expect(content).toContain("citations:");
|
|
50
|
+
expect(content).toContain("entity_refs:");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("summary signal fixture has required signal fields", () => {
|
|
54
|
+
const content = readFileSync(SIGNAL_SUMMARY_PATH, "utf-8");
|
|
55
|
+
expect(content).toContain("signal_type: summary");
|
|
56
|
+
expect(content).toContain("source_ref:");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// S3 mock round-trip tests
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
describe("mockS3WithEntries — listing", () => {
|
|
65
|
+
it("lists all entries when no prefix filter is applied", async () => {
|
|
66
|
+
const sourceContent = readFileSync(SOURCE_PATH, "utf-8");
|
|
67
|
+
const signalContent = readFileSync(SIGNAL_ACTION_PATH, "utf-8");
|
|
68
|
+
|
|
69
|
+
const client = mockS3WithEntries({
|
|
70
|
+
entries: [
|
|
71
|
+
{ key: "sources/meetings/abc.md", content: sourceContent },
|
|
72
|
+
{ key: "signals/action_item/xyz.md", content: signalContent },
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const result = await client.send(
|
|
77
|
+
new ListObjectsV2Command({ Bucket: "mock-bucket" }),
|
|
78
|
+
) as { Contents: Array<{ Key: string; Size: number }> };
|
|
79
|
+
|
|
80
|
+
expect(result.Contents).toHaveLength(2);
|
|
81
|
+
const keys = result.Contents.map((c) => c.Key);
|
|
82
|
+
expect(keys).toContain("sources/meetings/abc.md");
|
|
83
|
+
expect(keys).toContain("signals/action_item/xyz.md");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("filters by prefix — returns only matching entries", async () => {
|
|
87
|
+
const sourceContent = readFileSync(SOURCE_PATH, "utf-8");
|
|
88
|
+
const signalContent = readFileSync(SIGNAL_ACTION_PATH, "utf-8");
|
|
89
|
+
|
|
90
|
+
const client = mockS3WithEntries({
|
|
91
|
+
entries: [
|
|
92
|
+
{ key: "sources/meetings/abc.md", content: sourceContent },
|
|
93
|
+
{ key: "signals/action_item/xyz.md", content: signalContent },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const sourceList = await client.send(
|
|
98
|
+
new ListObjectsV2Command({ Bucket: "mock-bucket", Prefix: "sources/meetings/" }),
|
|
99
|
+
) as { Contents: Array<{ Key: string }> };
|
|
100
|
+
|
|
101
|
+
expect(sourceList.Contents).toHaveLength(1);
|
|
102
|
+
expect(sourceList.Contents[0].Key).toBe("sources/meetings/abc.md");
|
|
103
|
+
|
|
104
|
+
const signalList = await client.send(
|
|
105
|
+
new ListObjectsV2Command({ Bucket: "mock-bucket", Prefix: "signals/action_item/" }),
|
|
106
|
+
) as { Contents: Array<{ Key: string }> };
|
|
107
|
+
|
|
108
|
+
expect(signalList.Contents).toHaveLength(1);
|
|
109
|
+
expect(signalList.Contents[0].Key).toBe("signals/action_item/xyz.md");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("mockS3WithEntries — get round-trip", () => {
|
|
114
|
+
it("retrieves source content and preserves exact bytes", async () => {
|
|
115
|
+
const sourceContent = readFileSync(SOURCE_PATH, "utf-8");
|
|
116
|
+
|
|
117
|
+
const client = mockS3WithEntries({
|
|
118
|
+
entries: [{ key: "sources/meetings/abc.md", content: sourceContent }],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const result = await client.send(
|
|
122
|
+
new GetObjectCommand({ Bucket: "mock-bucket", Key: "sources/meetings/abc.md" }),
|
|
123
|
+
) as { Body: AsyncIterable<Uint8Array> };
|
|
124
|
+
|
|
125
|
+
const chunks: Buffer[] = [];
|
|
126
|
+
for await (const chunk of result.Body) {
|
|
127
|
+
chunks.push(Buffer.from(chunk));
|
|
128
|
+
}
|
|
129
|
+
const retrieved = Buffer.concat(chunks).toString("utf-8");
|
|
130
|
+
|
|
131
|
+
expect(retrieved).toBe(sourceContent);
|
|
132
|
+
// Spot-check frontmatter field survives round-trip
|
|
133
|
+
expect(retrieved).toContain("source_id: abc-meeting-001");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("retrieves signal content and preserves exact bytes", async () => {
|
|
137
|
+
const signalContent = readFileSync(SIGNAL_ACTION_PATH, "utf-8");
|
|
138
|
+
|
|
139
|
+
const client = mockS3WithEntries({
|
|
140
|
+
entries: [{ key: "signals/action_item/xyz.md", content: signalContent }],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const result = await client.send(
|
|
144
|
+
new GetObjectCommand({ Bucket: "mock-bucket", Key: "signals/action_item/xyz.md" }),
|
|
145
|
+
) as { Body: AsyncIterable<Uint8Array> };
|
|
146
|
+
|
|
147
|
+
const chunks: Buffer[] = [];
|
|
148
|
+
for await (const chunk of result.Body) {
|
|
149
|
+
chunks.push(Buffer.from(chunk));
|
|
150
|
+
}
|
|
151
|
+
const retrieved = Buffer.concat(chunks).toString("utf-8");
|
|
152
|
+
|
|
153
|
+
expect(retrieved).toBe(signalContent);
|
|
154
|
+
expect(retrieved).toContain("signal_type: action_item");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("throws NoSuchKey for missing entry", async () => {
|
|
158
|
+
const client = mockS3WithEntries({ entries: [] });
|
|
159
|
+
|
|
160
|
+
await expect(
|
|
161
|
+
client.send(new GetObjectCommand({ Bucket: "mock-bucket", Key: "does/not/exist.md" })),
|
|
162
|
+
).rejects.toMatchObject({ name: "NoSuchKey" });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Vault-service mock tests
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
describe("mockVaultService — membership endpoint", () => {
|
|
171
|
+
let restore: (() => void) | undefined;
|
|
172
|
+
|
|
173
|
+
afterEach(() => {
|
|
174
|
+
restore?.();
|
|
175
|
+
restore = undefined;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("GET /membership/me returns memberships for all configured entities", async () => {
|
|
179
|
+
restore = mockVaultService({
|
|
180
|
+
entities: [
|
|
181
|
+
{ uid: "cmp_indigo_001", slug: "indigo", bucketName: "hq-indigo-bucket" },
|
|
182
|
+
{ uid: "prs_personal_001", slug: "personal", bucketName: "hq-personal-bucket" },
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const res = await fetch("https://vault-api.example.com/membership/me");
|
|
187
|
+
const body = (await res.json()) as { memberships: Array<{ companyUid: string }> };
|
|
188
|
+
|
|
189
|
+
expect(res.status).toBe(200);
|
|
190
|
+
expect(body.memberships).toHaveLength(2);
|
|
191
|
+
const uids = body.memberships.map((m) => m.companyUid);
|
|
192
|
+
expect(uids).toContain("cmp_indigo_001");
|
|
193
|
+
expect(uids).toContain("prs_personal_001");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("POST /entities/{uid}/sts returns credentials", async () => {
|
|
197
|
+
restore = mockVaultService({
|
|
198
|
+
entities: [{ uid: "cmp_indigo_001", slug: "indigo", bucketName: "hq-indigo-bucket" }],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const res = await fetch("https://vault-api.example.com/entities/cmp_indigo_001/sts", {
|
|
202
|
+
method: "POST",
|
|
203
|
+
});
|
|
204
|
+
const body = (await res.json()) as {
|
|
205
|
+
credentials: { accessKeyId: string };
|
|
206
|
+
bucketName: string;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
expect(res.status).toBe(200);
|
|
210
|
+
expect(body.credentials.accessKeyId).toBe("ASIAMOCK000000000001");
|
|
211
|
+
expect(body.bucketName).toBe("hq-indigo-bucket");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// CLI runner
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
describe("runCli — basic wiring", () => {
|
|
220
|
+
it("runCli(['hq', '--help']) exits 0 and emits non-empty stdout", async () => {
|
|
221
|
+
const result = await runCli(["hq", "--help"]);
|
|
222
|
+
expect(result.exitCode).toBe(0);
|
|
223
|
+
expect(result.stdout.length).toBeGreaterThan(0);
|
|
224
|
+
expect(result.stdout).toContain("hq");
|
|
225
|
+
});
|
|
226
|
+
});
|