@liquidmetal-ai/raindrop 0.9.7 → 0.10.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 (129) hide show
  1. package/README.md +86 -86
  2. package/bundle/{chunk-AWVSG5HH.js → chunk-2GD7Q3YS.js} +1 -1
  3. package/bundle/{chunk-FYMUM7KM.js → chunk-2QH4PO44.js} +1 -1
  4. package/bundle/{chunk-2KEFV5U5.js → chunk-3DZWEZ2R.js} +1 -1
  5. package/bundle/{chunk-FFKINGT5.js → chunk-45V47YNS.js} +4 -4
  6. package/bundle/{chunk-ILG4QJPW.js → chunk-4FXE4I6S.js} +2 -2
  7. package/bundle/{chunk-43UGYZQA.js → chunk-4RN4TIJI.js} +1 -1
  8. package/bundle/{chunk-OHP3SQGY.js → chunk-4U2CSHCN.js} +1 -1
  9. package/bundle/{chunk-T3VHOMDJ.js → chunk-4UUTGMDZ.js} +1 -1
  10. package/bundle/{chunk-6VCATADU.js → chunk-4WNBHWJP.js} +4 -4
  11. package/bundle/{chunk-CEFC4JXX.js → chunk-5KMI72AS.js} +1 -1
  12. package/bundle/{chunk-2HWWLWLM.js → chunk-5REFCZTG.js} +2 -2
  13. package/bundle/{chunk-ZEKMKKMJ.js → chunk-6BCRCAR4.js} +1 -1
  14. package/bundle/{chunk-YWKX37S2.js → chunk-7GDFE7HE.js} +3 -1
  15. package/bundle/{chunk-FV4QERYA.js → chunk-AI5PKHR7.js} +1 -1
  16. package/bundle/{chunk-WTTFYUHL.js → chunk-D5MXH3QV.js} +1 -1
  17. package/bundle/{chunk-GBFY2PMX.js → chunk-ETDAUGBG.js} +2 -2
  18. package/bundle/{chunk-O75RZNCU.js → chunk-GBUNP7OT.js} +2 -2
  19. package/bundle/{chunk-Z2VVSBIX.js → chunk-GRUPCK5H.js} +4 -4
  20. package/bundle/{chunk-XN52RPCA.js → chunk-JENN4EVA.js} +2 -2
  21. package/bundle/{chunk-KYP4ZWG7.js → chunk-JZYHHRGL.js} +2 -2
  22. package/bundle/{chunk-VW5YPIKL.js → chunk-LJKAU7FY.js} +30 -2
  23. package/bundle/{chunk-DSP542FN.js → chunk-MJZG4ABT.js} +1 -1
  24. package/bundle/{chunk-T7WFDN3I.js → chunk-MZZHXHB4.js} +4 -4
  25. package/bundle/{chunk-RUORC7A6.js → chunk-ORUMKXQZ.js} +2 -2
  26. package/bundle/{chunk-B4MQISI4.js → chunk-PMHLHYMI.js} +4 -4
  27. package/bundle/{chunk-4NTUYQ5R.js → chunk-QRJUX37T.js} +1 -1
  28. package/bundle/chunk-QTCJ6YYG.js +147 -0
  29. package/bundle/{chunk-E7J2LWQ5.js → chunk-RAPSKVRO.js} +1 -1
  30. package/bundle/{chunk-BC3Z5E5X.js → chunk-U7NHRBYD.js} +1 -1
  31. package/bundle/{chunk-QN2OJEAL.js → chunk-WDR5M2SS.js} +25 -25
  32. package/bundle/{chunk-SPBJ5BNI.js → chunk-YXJU5KSB.js} +1 -1
  33. package/bundle/{chunk-6AU7I5YZ.js → chunk-ZQIPU6IX.js} +2 -2
  34. package/bundle/commands/annotation/get.js +3 -3
  35. package/bundle/commands/annotation/list.js +3 -3
  36. package/bundle/commands/annotation/put.js +3 -3
  37. package/bundle/commands/auth/list.js +2 -2
  38. package/bundle/commands/auth/login.js +3 -3
  39. package/bundle/commands/auth/logout.js +2 -2
  40. package/bundle/commands/auth/select.js +3 -3
  41. package/bundle/commands/bucket/create-credential.js +2 -2
  42. package/bundle/commands/bucket/delete-credential.js +2 -2
  43. package/bundle/commands/bucket/get-credential.js +2 -2
  44. package/bundle/commands/bucket/list-credentials.js +2 -2
  45. package/bundle/commands/build/branch.js +9 -9
  46. package/bundle/commands/build/checkout.js +6 -6
  47. package/bundle/commands/build/clone.js +7 -7
  48. package/bundle/commands/build/delete.js +6 -6
  49. package/bundle/commands/build/deploy.js +9 -9
  50. package/bundle/commands/build/env/get.js +2 -2
  51. package/bundle/commands/build/env/set.js +2 -2
  52. package/bundle/commands/build/find.js +4 -4
  53. package/bundle/commands/build/generate.js +3 -3
  54. package/bundle/commands/build/init-workspace.js +3 -3
  55. package/bundle/commands/build/init.js +3 -3
  56. package/bundle/commands/build/list.js +5 -5
  57. package/bundle/commands/build/sandbox.js +7 -7
  58. package/bundle/commands/build/start.js +2 -2
  59. package/bundle/commands/build/status.js +5 -5
  60. package/bundle/commands/build/stop.js +2 -2
  61. package/bundle/commands/build/tools/check.js +2 -2
  62. package/bundle/commands/build/tools/fmt.js +2 -2
  63. package/bundle/commands/build/unsandbox.js +7 -7
  64. package/bundle/commands/build/upload.js +5 -5
  65. package/bundle/commands/build/validate.js +4 -4
  66. package/bundle/commands/build/workos/delete.js +4 -4
  67. package/bundle/commands/build/workos/env/attach.js +3 -3
  68. package/bundle/commands/build/workos/env/create.js +3 -3
  69. package/bundle/commands/build/workos/env/delete.js +3 -3
  70. package/bundle/commands/build/workos/env/detach.js +3 -3
  71. package/bundle/commands/build/workos/env/get.js +3 -3
  72. package/bundle/commands/build/workos/env/list.js +3 -3
  73. package/bundle/commands/build/workos/env/set.js +3 -3
  74. package/bundle/commands/build/workos/invite.js +3 -3
  75. package/bundle/commands/build/workos/setup.js +3 -3
  76. package/bundle/commands/build/workos/status.js +3 -3
  77. package/bundle/commands/dns/create.js +2 -2
  78. package/bundle/commands/dns/delete.js +4 -4
  79. package/bundle/commands/dns/get.js +4 -4
  80. package/bundle/commands/dns/list.js +3 -3
  81. package/bundle/commands/dns/records/create.js +2 -2
  82. package/bundle/commands/dns/records/delete.js +3 -3
  83. package/bundle/commands/dns/records/get.js +2 -2
  84. package/bundle/commands/dns/records/list.js +2 -2
  85. package/bundle/commands/dns/records/update.js +2 -2
  86. package/bundle/commands/logs/query.js +3 -3
  87. package/bundle/commands/logs/tail.js +3 -3
  88. package/bundle/commands/mcp/install-claude.js +2 -2
  89. package/bundle/commands/mcp/install-gemini.js +2 -2
  90. package/bundle/commands/mcp/install-goose.js +2 -2
  91. package/bundle/commands/mcp/status.js +2 -2
  92. package/bundle/commands/object/delete.js +2 -2
  93. package/bundle/commands/object/get.js +2 -2
  94. package/bundle/commands/object/list.js +73 -7
  95. package/bundle/commands/object/put.js +2 -2
  96. package/bundle/commands/query/chunk-search.js +3 -3
  97. package/bundle/commands/query/document.js +3 -3
  98. package/bundle/commands/query/events.js +2 -2
  99. package/bundle/commands/query/reindex.js +2 -2
  100. package/bundle/commands/query/search.js +3 -3
  101. package/bundle/commands/tail.js +2 -2
  102. package/bundle/index.js +1 -1
  103. package/dist/build.test.d.ts +2 -0
  104. package/dist/build.test.d.ts.map +1 -0
  105. package/dist/build.test.js +46 -0
  106. package/dist/codegen.test.d.ts +2 -0
  107. package/dist/codegen.test.d.ts.map +1 -0
  108. package/dist/codegen.test.js +223 -0
  109. package/dist/commands/logs/tail.test.d.ts +2 -0
  110. package/dist/commands/logs/tail.test.d.ts.map +1 -0
  111. package/dist/commands/logs/tail.test.js +366 -0
  112. package/dist/commands/object/list.d.ts.map +1 -1
  113. package/dist/commands/object/list.js +91 -4
  114. package/dist/config.test.d.ts +2 -0
  115. package/dist/config.test.d.ts.map +1 -0
  116. package/dist/config.test.js +27 -0
  117. package/dist/index.test.d.ts +2 -0
  118. package/dist/index.test.d.ts.map +1 -0
  119. package/dist/index.test.js +56 -0
  120. package/oclif.manifest.json +2058 -2058
  121. package/package.json +4 -4
  122. package/templates/db/node_modules/.bin/prisma +2 -2
  123. package/templates/db/node_modules/.bin/prisma-kysely +2 -2
  124. package/templates/db/node_modules/.bin/tsc +3 -7
  125. package/templates/db/node_modules/.bin/tsserver +3 -7
  126. package/templates/db/node_modules/.bin/zx +2 -2
  127. package/templates/init/RAINDROP.md.hbs +26 -22
  128. package/templates/init/src/_app/cors.ts +5 -4
  129. package/bundle/chunk-B6JLI47W.js +0 -315
@@ -0,0 +1,223 @@
1
+ import { mustManifestFromString } from '@liquidmetal-ai/drizzle/appify/index';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { expect, test } from 'vitest';
6
+ import { codegenPlan, gatherEnvForHandler, kebabCaseToCamelCase, kebabCaseToConstantCase, kebabCaseToUpperCamelCase, renderTemplatesFromDirectory, shouldWriteTemplate, trimTemplateSuffix, } from './codegen.js';
7
+ test('trims handlebars suffix', async () => {
8
+ expect(trimTemplateSuffix('index.ts.hbs')).toEqual('index.ts');
9
+ });
10
+ test('codegens from blank app', async () => {
11
+ const apps = await mustManifestFromString(`
12
+ application "ingest" {}
13
+ `);
14
+ const plan = codegenPlan(apps);
15
+ expect(Object.keys(plan)).toHaveLength(1);
16
+ expect(plan[0]?.templateName).toMatch('templates/init');
17
+ expect(plan[0]?.outPath).toEqual('');
18
+ });
19
+ test('codegens from app with observer', async () => {
20
+ const apps = await mustManifestFromString(`
21
+ application "ingest" {
22
+ bucket "bucket" {}
23
+
24
+ observer "process" {
25
+ source {
26
+ bucket = "bucket"
27
+ }
28
+ }
29
+ }
30
+ `);
31
+ const plan = codegenPlan(apps);
32
+ expect(Object.keys(plan)).toHaveLength(2);
33
+ expect(plan[1]?.templateName).toMatch('templates/handlers/bucket-event-notification');
34
+ expect(plan[1]?.outPath).toEqual('src/process');
35
+ });
36
+ test('codegens from app with service', async () => {
37
+ const apps = await mustManifestFromString(`
38
+ application "ingest" {
39
+ bucket "bucket" {}
40
+
41
+ service "ingester" {
42
+ }
43
+ }
44
+ `);
45
+ const plan = codegenPlan(apps);
46
+ expect(Object.keys(plan)).toHaveLength(2);
47
+ expect(plan[1]?.templateName).toMatch('templates/handlers/http-service');
48
+ expect(plan[1]?.outPath).toEqual('src/ingester');
49
+ });
50
+ test('codegens from app with durable object', async () => {
51
+ const apps = await mustManifestFromString(`
52
+ application "ingest" {
53
+ actor "ingest-object" {
54
+ }
55
+ }
56
+ `);
57
+ const plan = codegenPlan(apps);
58
+ expect(Object.keys(plan)).toHaveLength(2);
59
+ expect(plan[1]?.templateName).toMatch('templates/handlers/actor');
60
+ expect(plan[1]?.outPath).toEqual('src/ingest-object');
61
+ });
62
+ test('codegens from app with sql database', async () => {
63
+ const apps = await mustManifestFromString(`
64
+ application "ingest" {
65
+ sql_database "demosql" {}
66
+ }
67
+ `);
68
+ const plan = codegenPlan(apps);
69
+ expect(Object.keys(plan)).toHaveLength(6);
70
+ expect(plan[0]?.templateName).toMatch('templates/init');
71
+ expect(plan[0]?.outPath).toEqual('');
72
+ expect(plan[1]?.templateName).toMatch('templates/db/package.json');
73
+ expect(plan[1]?.outPath).toEqual('./');
74
+ expect(plan[2]?.templateName).toMatch('templates/db/db');
75
+ expect(plan[2]?.outPath).toEqual('db/demosql');
76
+ expect(plan[3]?.templateName).toMatch('templates/db/common');
77
+ expect(plan[3]?.outPath).toEqual('src/common');
78
+ expect(plan[4]?.templateName).toMatch('templates/db/prisma');
79
+ expect(plan[4]?.outPath).toEqual('prisma/demosql');
80
+ expect(plan[5]?.templateName).toMatch('templates/db/scripts');
81
+ expect(plan[5]?.outPath).toEqual('scripts');
82
+ });
83
+ test('only codegens scaffolding when enabled', () => {
84
+ expect(shouldWriteTemplate({
85
+ renderScaffoldingCode: true,
86
+ }, 'src/handlers/something/__index.ts')).toBe(true);
87
+ expect(shouldWriteTemplate({
88
+ renderScaffoldingCode: false,
89
+ }, 'src/handlers/something/__index.ts')).toBe(false);
90
+ });
91
+ test('only codegens user modifiable code when enabled', () => {
92
+ expect(shouldWriteTemplate({
93
+ renderUserModifiableCode: true,
94
+ }, 'src/handlers/something/index.ts')).toBe(true);
95
+ expect(shouldWriteTemplate({
96
+ renderUserModifiableCode: false,
97
+ }, 'src/handlers/something/index.ts')).toBe(false);
98
+ });
99
+ test('only codegens non-user modifiable code when enabled', () => {
100
+ expect(shouldWriteTemplate({
101
+ renderNonUserModifiableCode: true,
102
+ }, 'src/handlers/something/env.gen.d.ts')).toBe(true);
103
+ expect(shouldWriteTemplate({
104
+ renderNonUserModifiableCode: false,
105
+ }, 'src/handlers/something/env.gen.d.ts')).toBe(false);
106
+ });
107
+ test('codegens template files', async () => {
108
+ const templatePath = path.join(__dirname, '..', 'templates', 'init');
109
+ const outPath = await fs.mkdtemp(path.join(os.tmpdir(), 'codegen-test'));
110
+ await renderTemplatesFromDirectory({
111
+ templatePath,
112
+ outPath,
113
+ context: {
114
+ toolName: 'raindrop',
115
+ applicationName: 'test-app',
116
+ raindropFrameworkVersion: '0.4.9',
117
+ },
118
+ opts: {
119
+ renderScaffoldingCode: true,
120
+ renderUserModifiableCode: true,
121
+ renderNonUserModifiableCode: true,
122
+ },
123
+ });
124
+ // Verify nested directory structure is preserved
125
+ const authFilePath = path.join(outPath, 'src', '_app', 'auth.ts');
126
+ const authFileExists = await fs.access(authFilePath).then(() => true, () => false);
127
+ expect(authFileExists).toBe(true);
128
+ // Verify the file content was copied correctly
129
+ const authFileContent = await fs.readFile(authFilePath, 'utf-8');
130
+ expect(authFileContent).toContain('verifyIssuer');
131
+ expect(authFileContent).toContain('requireAuthenticated');
132
+ });
133
+ test('kebabCaseToConstantCase', () => {
134
+ expect(kebabCaseToConstantCase('my-variable')).toEqual('MY_VARIABLE');
135
+ });
136
+ test('kebabCaseToCamelCase', () => {
137
+ expect(kebabCaseToCamelCase('my-variable')).toEqual('myVariable');
138
+ });
139
+ test('kebabCaseToUpperCamelCase', () => {
140
+ expect(kebabCaseToUpperCamelCase('my-variable')).toEqual('MyVariable');
141
+ });
142
+ test('generates SmartMemory type bindings', async () => {
143
+ const apps = await mustManifestFromString(`
144
+ application "test-app" {
145
+ smartmemory "user-memory" {}
146
+
147
+ service "test-service" {}
148
+ }
149
+ `);
150
+ const app = apps[0];
151
+ const service = app.service[0];
152
+ const generated = gatherEnvForHandler(service, app);
153
+ // Should include SmartMemory import
154
+ expect(generated).toMatch(/import\s+{[^}]*SmartMemory[^}]*}\s+from\s+'@liquidmetal-ai\/raindrop-framework'/);
155
+ // Should include SmartMemory type binding with constant case naming
156
+ expect(generated).toMatch(/USER_MEMORY:\s*SmartMemory/);
157
+ });
158
+ test('generates SmartBucket type bindings', async () => {
159
+ const apps = await mustManifestFromString(`
160
+ application "test-app" {
161
+ smartbucket "data-bucket" {}
162
+
163
+ service "test-service" {}
164
+ }
165
+ `);
166
+ const app = apps[0];
167
+ const service = app.service[0];
168
+ const generated = gatherEnvForHandler(service, app);
169
+ // Should include SmartBucket import
170
+ expect(generated).toMatch(/import\s+{[^}]*SmartBucket[^}]*}\s+from\s+'@liquidmetal-ai\/raindrop-framework'/);
171
+ // Should include SmartBucket type binding with constant case naming
172
+ expect(generated).toMatch(/DATA_BUCKET:\s*SmartBucket/);
173
+ });
174
+ test('always includes mem KvCache binding', async () => {
175
+ const apps = await mustManifestFromString(`
176
+ application "test-app" {
177
+ service "test-service" {}
178
+ }
179
+ `);
180
+ const app = apps[0];
181
+ const service = app.service[0];
182
+ const generated = gatherEnvForHandler(service, app);
183
+ // Should include KvCache import
184
+ expect(generated).toMatch(/import\s+{[^}]*KvCache[^}]*}\s+from\s+'@liquidmetal-ai\/raindrop-framework'/);
185
+ // Should include mem type binding
186
+ expect(generated).toMatch(/mem:\s*KvCache/);
187
+ });
188
+ test('includes auth env vars for protected service', async () => {
189
+ const apps = await mustManifestFromString(`
190
+ application "test-app" {
191
+ service "protected-service" {
192
+ visibility = "protected"
193
+ }
194
+ }
195
+ `);
196
+ const app = apps[0];
197
+ const service = app.service[0];
198
+ const generated = gatherEnvForHandler(service, app);
199
+ // Should include core auth bindings and WorkOS provider bindings for protected services
200
+ expect(generated).toMatch(/LM_AUTH_ALLOWED_ORIGINS:\s*string/);
201
+ expect(generated).toMatch(/LM_AUTH_ALLOWED_ISSUERS:\s*string/);
202
+ expect(generated).toMatch(/LM_PROVIDER_WORKOS_ORG_ID\?:\s*string/);
203
+ expect(generated).toMatch(/LM_PROVIDER_WORKOS_CLIENT_ID\?:\s*string/);
204
+ expect(generated).toMatch(/LM_PROVIDER_WORKOS_API_KEY\?:\s*string/);
205
+ });
206
+ test('does not include auth env vars for public service', async () => {
207
+ const apps = await mustManifestFromString(`
208
+ application "test-app" {
209
+ service "public-service" {
210
+ visibility = "public"
211
+ }
212
+ }
213
+ `);
214
+ const app = apps[0];
215
+ const service = app.service[0];
216
+ const generated = gatherEnvForHandler(service, app);
217
+ // Should NOT include auth env vars for non-protected services
218
+ expect(generated).not.toMatch(/LM_AUTH_ALLOWED_ORIGINS/);
219
+ expect(generated).not.toMatch(/LM_AUTH_ALLOWED_ISSUERS/);
220
+ expect(generated).not.toMatch(/LM_PROVIDER_WORKOS_ORG_ID/);
221
+ expect(generated).not.toMatch(/LM_PROVIDER_WORKOS_CLIENT_ID/);
222
+ expect(generated).not.toMatch(/LM_PROVIDER_WORKOS_API_KEY/);
223
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tail.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tail.test.d.ts","sourceRoot":"","sources":["../../../src/commands/logs/tail.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,366 @@
1
+ import { create } from '@bufbuild/protobuf';
2
+ import { QueryLogsResponseSchema, StreamLogsResponseSchema } from '@liquidmetal-ai/drizzle/liquidmetal/v1alpha1/riverjack_pb';
3
+ import { expect, test, vi, describe, beforeEach, afterEach } from 'vitest';
4
+ import Tail from './tail.js';
5
+ // Mock valueOf function
6
+ vi.mock('@liquidmetal-ai/drizzle/appify/build', () => ({
7
+ valueOf: (obj) => obj?.value || obj,
8
+ }));
9
+ // Mock the base command dependencies
10
+ vi.mock('../../base-command.js', () => {
11
+ return {
12
+ BaseCommand: class {
13
+ static HIDDEN_FLAGS = {};
14
+ flags = {};
15
+ log = vi.fn();
16
+ error = vi.fn((msg, options) => {
17
+ if (options?.exit) {
18
+ throw new Error(msg);
19
+ }
20
+ });
21
+ async loadManifest() {
22
+ return [{ name: { value: 'test-app' } }];
23
+ }
24
+ async loadConfig() {
25
+ return { versionId: 'test-version-123' };
26
+ }
27
+ async tenantRiverjackService() {
28
+ return {
29
+ userId: 'test-user',
30
+ organizationId: 'test-org',
31
+ client: this.mockRiverjackClient,
32
+ };
33
+ }
34
+ mockRiverjackClient = {
35
+ queryLogs: vi.fn(),
36
+ streamLogs: vi.fn(),
37
+ };
38
+ }
39
+ };
40
+ });
41
+ // Mock the log helpers
42
+ vi.mock('../../log-helpers.js', () => ({
43
+ displayTraceGroupedEvents: vi.fn(),
44
+ }));
45
+ describe('Tail command', () => {
46
+ let tail;
47
+ let mockQueryLogs;
48
+ let mockStreamLogs;
49
+ let logSpy;
50
+ beforeEach(() => {
51
+ tail = new Tail([], {});
52
+ logSpy = vi.spyOn(tail, 'log');
53
+ // Access the mock client through the instance
54
+ const mockClient = tail.mockRiverjackClient;
55
+ mockQueryLogs = mockClient.queryLogs;
56
+ mockStreamLogs = mockClient.streamLogs;
57
+ // Reset mocks
58
+ vi.clearAllMocks();
59
+ });
60
+ afterEach(() => {
61
+ vi.restoreAllMocks();
62
+ });
63
+ describe('GNU tail behavior', () => {
64
+ test('default behavior: shows 10 logs and exits (no streaming)', async () => {
65
+ // Setup mock response
66
+ const mockEvents = [
67
+ { traceId: '1', message: 'log 1' },
68
+ { traceId: '2', message: 'log 2' },
69
+ ];
70
+ mockQueryLogs.mockResolvedValue(create(QueryLogsResponseSchema, {
71
+ events: mockEvents,
72
+ totalCount: BigInt(2),
73
+ hasMore: false,
74
+ }));
75
+ // Set no flags (default behavior)
76
+ tail.flags = {
77
+ application: 'test-app',
78
+ output: 'text',
79
+ };
80
+ await tail.run();
81
+ // Should query for 10 logs (default)
82
+ expect(mockQueryLogs).toHaveBeenCalledWith(expect.objectContaining({
83
+ limit: 10,
84
+ organizationId: 'test-org',
85
+ userId: 'test-user',
86
+ applicationName: 'test-app',
87
+ applicationVersionId: 'test-version-123',
88
+ }));
89
+ // Should NOT start streaming
90
+ expect(mockStreamLogs).not.toHaveBeenCalled();
91
+ // Should show appropriate messages
92
+ expect(logSpy).toHaveBeenCalledWith('Using organization: test-org');
93
+ expect(logSpy).toHaveBeenCalledWith('Using user: test-user');
94
+ expect(logSpy).toHaveBeenCalledWith('Showing last 10 logs for test-app@test-version-123...');
95
+ expect(logSpy).toHaveBeenCalledWith('📜 Showing last 2 log events:');
96
+ // Should NOT show streaming messages
97
+ expect(logSpy).not.toHaveBeenCalledWith(expect.stringContaining('Press Ctrl+C'));
98
+ expect(logSpy).not.toHaveBeenCalledWith(expect.stringContaining('Live stream starting'));
99
+ });
100
+ test('with -n flag: shows N logs and exits', async () => {
101
+ const mockEvents = Array(5).fill(null).map((_, i) => ({
102
+ traceId: `${i}`,
103
+ message: `log ${i}`,
104
+ }));
105
+ mockQueryLogs.mockResolvedValue(create(QueryLogsResponseSchema, {
106
+ events: mockEvents,
107
+ totalCount: BigInt(5),
108
+ hasMore: false,
109
+ }));
110
+ tail.flags = {
111
+ application: 'test-app',
112
+ output: 'text',
113
+ lines: 25,
114
+ };
115
+ await tail.run();
116
+ // Should query for 25 logs
117
+ expect(mockQueryLogs).toHaveBeenCalledWith(expect.objectContaining({
118
+ limit: 25,
119
+ }));
120
+ // Should NOT stream
121
+ expect(mockStreamLogs).not.toHaveBeenCalled();
122
+ // Should show correct message
123
+ expect(logSpy).toHaveBeenCalledWith('Showing last 25 logs for test-app@test-version-123...');
124
+ });
125
+ test('with -f flag: shows 10 logs then streams', async () => {
126
+ const mockEvents = [{ traceId: '1', message: 'log 1' }];
127
+ mockQueryLogs.mockResolvedValue(create(QueryLogsResponseSchema, {
128
+ events: mockEvents,
129
+ totalCount: BigInt(1),
130
+ hasMore: false,
131
+ }));
132
+ // Mock async generator for streaming
133
+ const mockStream = async function* () {
134
+ yield create(StreamLogsResponseSchema, {
135
+ responseType: 'stream_started',
136
+ message: 'Stream started',
137
+ });
138
+ };
139
+ mockStreamLogs.mockReturnValue(mockStream());
140
+ tail.flags = {
141
+ application: 'test-app',
142
+ output: 'text',
143
+ follow: true,
144
+ };
145
+ await tail.run();
146
+ // Should query for 10 logs (default)
147
+ expect(mockQueryLogs).toHaveBeenCalledWith(expect.objectContaining({
148
+ limit: 10,
149
+ }));
150
+ // Should start streaming
151
+ expect(mockStreamLogs).toHaveBeenCalled();
152
+ // Should show appropriate messages
153
+ expect(logSpy).toHaveBeenCalledWith('Following logs for test-app@test-version-123...');
154
+ expect(logSpy).toHaveBeenCalledWith('Press Ctrl+C to stop following\n');
155
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Live stream starting'));
156
+ });
157
+ test('with -n and -f flags: shows N logs then streams', async () => {
158
+ const mockEvents = Array(30).fill(null).map((_, i) => ({
159
+ traceId: `${i}`,
160
+ message: `log ${i}`,
161
+ }));
162
+ mockQueryLogs.mockResolvedValue(create(QueryLogsResponseSchema, {
163
+ events: mockEvents,
164
+ totalCount: BigInt(30),
165
+ hasMore: false,
166
+ }));
167
+ const mockStream = async function* () {
168
+ yield create(StreamLogsResponseSchema, {
169
+ responseType: 'stream_started',
170
+ });
171
+ };
172
+ mockStreamLogs.mockReturnValue(mockStream());
173
+ tail.flags = {
174
+ application: 'test-app',
175
+ output: 'text',
176
+ lines: 30,
177
+ follow: true,
178
+ };
179
+ await tail.run();
180
+ // Should query for 30 logs
181
+ expect(mockQueryLogs).toHaveBeenCalledWith(expect.objectContaining({
182
+ limit: 30,
183
+ }));
184
+ // Should start streaming
185
+ expect(mockStreamLogs).toHaveBeenCalled();
186
+ // Should show correct messages
187
+ expect(logSpy).toHaveBeenCalledWith('Following logs for test-app@test-version-123...');
188
+ expect(logSpy).toHaveBeenCalledWith('📜 Showing last 30 log events:');
189
+ });
190
+ });
191
+ describe('edge cases', () => {
192
+ test('handles no logs found gracefully', async () => {
193
+ mockQueryLogs.mockResolvedValue(create(QueryLogsResponseSchema, {
194
+ events: [],
195
+ totalCount: BigInt(0),
196
+ hasMore: false,
197
+ }));
198
+ tail.flags = {
199
+ application: 'test-app',
200
+ output: 'text',
201
+ lines: 10,
202
+ };
203
+ await tail.run();
204
+ expect(logSpy).toHaveBeenCalledWith('ℹ️ No recent logs found in the last hour');
205
+ expect(mockStreamLogs).not.toHaveBeenCalled();
206
+ });
207
+ test('handles query error gracefully when following', async () => {
208
+ mockQueryLogs.mockRejectedValue(new Error('Query failed'));
209
+ const mockStream = async function* () {
210
+ yield create(StreamLogsResponseSchema, {
211
+ responseType: 'stream_started',
212
+ });
213
+ };
214
+ mockStreamLogs.mockReturnValue(mockStream());
215
+ tail.flags = {
216
+ application: 'test-app',
217
+ output: 'text',
218
+ follow: true,
219
+ };
220
+ await tail.run();
221
+ // Should show error but continue to stream
222
+ expect(logSpy).toHaveBeenCalledWith('⚠️ Could not fetch recent logs: Query failed');
223
+ expect(logSpy).toHaveBeenCalledWith('🔴 Starting live stream anyway...');
224
+ expect(mockStreamLogs).toHaveBeenCalled();
225
+ });
226
+ test('handles query error gracefully when not following', async () => {
227
+ mockQueryLogs.mockRejectedValue(new Error('Query failed'));
228
+ tail.flags = {
229
+ application: 'test-app',
230
+ output: 'text',
231
+ lines: 10,
232
+ };
233
+ await tail.run();
234
+ // Should show error and exit
235
+ expect(logSpy).toHaveBeenCalledWith('⚠️ Could not fetch recent logs: Query failed');
236
+ expect(mockStreamLogs).not.toHaveBeenCalled();
237
+ });
238
+ test('handles -n 0 correctly (no recent logs)', async () => {
239
+ tail.flags = {
240
+ application: 'test-app',
241
+ output: 'text',
242
+ lines: 0,
243
+ };
244
+ await tail.run();
245
+ // Should not query for logs when lines is 0
246
+ expect(mockQueryLogs).not.toHaveBeenCalled();
247
+ expect(mockStreamLogs).not.toHaveBeenCalled();
248
+ expect(logSpy).toHaveBeenCalledWith('Showing last 0 logs for test-app@test-version-123...');
249
+ });
250
+ test('handles JSON output format', async () => {
251
+ const mockEvents = [
252
+ { traceId: '1', message: 'log 1' },
253
+ ];
254
+ mockQueryLogs.mockResolvedValue(create(QueryLogsResponseSchema, {
255
+ events: mockEvents,
256
+ totalCount: BigInt(1),
257
+ hasMore: false,
258
+ }));
259
+ tail.flags = {
260
+ application: 'test-app',
261
+ output: 'json',
262
+ lines: 1,
263
+ };
264
+ await tail.run();
265
+ // Should output JSON format
266
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('['));
267
+ });
268
+ });
269
+ describe('streaming behavior', () => {
270
+ test('handles different stream response types', async () => {
271
+ mockQueryLogs.mockResolvedValue(create(QueryLogsResponseSchema, {
272
+ events: [],
273
+ totalCount: BigInt(0),
274
+ hasMore: false,
275
+ }));
276
+ const mockStream = async function* () {
277
+ yield create(StreamLogsResponseSchema, {
278
+ responseType: 'stream_started',
279
+ message: 'Started',
280
+ timestamp: new Date().toISOString(),
281
+ });
282
+ yield create(StreamLogsResponseSchema, {
283
+ responseType: 'heartbeat',
284
+ });
285
+ yield create(StreamLogsResponseSchema, {
286
+ responseType: 'events',
287
+ events: [{ traceId: '1', message: 'streamed log' }],
288
+ });
289
+ yield create(StreamLogsResponseSchema, {
290
+ responseType: 'stream_ended',
291
+ message: 'Ended',
292
+ });
293
+ };
294
+ mockStreamLogs.mockReturnValue(mockStream());
295
+ tail.flags = {
296
+ application: 'test-app',
297
+ output: 'text',
298
+ follow: true,
299
+ };
300
+ await tail.run();
301
+ // Check that different response types are handled
302
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Started streaming logs'));
303
+ expect(logSpy).toHaveBeenCalledWith('✅ Stream ended');
304
+ });
305
+ test('handles stream errors', async () => {
306
+ mockQueryLogs.mockResolvedValue(create(QueryLogsResponseSchema, {
307
+ events: [],
308
+ totalCount: BigInt(0),
309
+ hasMore: false,
310
+ }));
311
+ // Mock error method to capture the error
312
+ const errorSpy = vi.spyOn(tail, 'error').mockImplementation((msg) => {
313
+ throw new Error(msg);
314
+ });
315
+ const mockStream = async function* () {
316
+ yield create(StreamLogsResponseSchema, {
317
+ responseType: 'service_error',
318
+ message: 'Stream connection failed',
319
+ });
320
+ throw new Error('Stream connection failed');
321
+ };
322
+ mockStreamLogs.mockReturnValue(mockStream());
323
+ tail.flags = {
324
+ application: 'test-app',
325
+ output: 'text',
326
+ follow: true,
327
+ };
328
+ // Should handle the error
329
+ await expect(tail.run()).rejects.toThrow('Failed to tail logs');
330
+ // Restore the mock
331
+ errorSpy.mockRestore();
332
+ });
333
+ });
334
+ describe('configuration', () => {
335
+ test('uses application from manifest when not provided', async () => {
336
+ mockQueryLogs.mockResolvedValue(create(QueryLogsResponseSchema, {
337
+ events: [],
338
+ totalCount: BigInt(0),
339
+ hasMore: false,
340
+ }));
341
+ tail.flags = {
342
+ output: 'text',
343
+ // No application flag set
344
+ };
345
+ await tail.run();
346
+ expect(mockQueryLogs).toHaveBeenCalledWith(expect.objectContaining({
347
+ applicationName: 'test-app', // From manifest
348
+ }));
349
+ });
350
+ test('uses provided application over manifest', async () => {
351
+ mockQueryLogs.mockResolvedValue(create(QueryLogsResponseSchema, {
352
+ events: [],
353
+ totalCount: BigInt(0),
354
+ hasMore: false,
355
+ }));
356
+ tail.flags = {
357
+ output: 'text',
358
+ application: 'custom-app',
359
+ };
360
+ await tail.run();
361
+ expect(mockQueryLogs).toHaveBeenCalledWith(expect.objectContaining({
362
+ applicationName: 'custom-app',
363
+ }));
364
+ });
365
+ });
366
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/commands/object/list.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEpD,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,WAAW,CAAC,OAAO,UAAU,CAAC;IACpE,MAAM,CAAC,WAAW,SAA8B;IAEhD,MAAM,CAAC,QAAQ,WAUb;IAEF,MAAM,CAAC,KAAK;;;;;;;;;;;;;;;MAgDV;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAqK3B"}
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/commands/object/list.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEpD,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,WAAW,CAAC,OAAO,UAAU,CAAC;IACpE,MAAM,CAAC,WAAW,SAA8B;IAEhD,MAAM,CAAC,QAAQ,WAUb;IAEF,MAAM,CAAC,KAAK;;;;;;;;;;;;;;;MAgDV;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CA0P3B"}