@pikku/core 0.12.99 → 0.12.101
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/CHANGELOG.md +48 -0
- package/dist/services/http-personas.d.ts +42 -7
- package/dist/services/http-personas.js +135 -12
- package/dist/services/persona-sign-in.d.ts +12 -10
- package/dist/services/persona-sign-in.js +26 -9
- package/dist/wirings/cli/cli-runner.js +2 -11
- package/dist/wirings/cli/format-cli-error.d.ts +24 -0
- package/dist/wirings/cli/format-cli-error.js +68 -0
- package/dist/wirings/persona/index.d.ts +1 -1
- package/dist/wirings/persona/index.js +1 -1
- package/dist/wirings/virtual-user/virtual-user-scaffold.d.ts +0 -1
- package/dist/wirings/virtual-user/virtual-user-scaffold.js +0 -4
- package/package.json +1 -1
- package/src/public-surface.json +1 -0
- package/src/services/http-personas-converse.test.ts +29 -20
- package/src/services/http-personas.test.ts +145 -0
- package/src/services/http-personas.ts +155 -12
- package/src/services/persona-sign-in.test.ts +22 -28
- package/src/services/persona-sign-in.ts +27 -19
- package/src/wirings/cli/cli-runner.test.ts +104 -0
- package/src/wirings/cli/cli-runner.ts +2 -11
- package/src/wirings/cli/format-cli-error.test.ts +91 -0
- package/src/wirings/cli/format-cli-error.ts +96 -0
- package/src/wirings/persona/index.ts +1 -0
- package/src/wirings/virtual-user/virtual-user-scaffold.ts +0 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { test, describe, beforeEach, afterEach } from 'node:test'
|
|
2
2
|
import * as assert from 'assert'
|
|
3
3
|
import { NotFoundError } from '../../errors/errors.js'
|
|
4
|
+
import { PikkuError } from '../../errors/error-handler.js'
|
|
4
5
|
import type { CorePikkuMiddleware } from '../../middleware/middleware.types.js'
|
|
5
6
|
import {
|
|
6
7
|
CLIError,
|
|
@@ -724,5 +725,108 @@ describe('CLI Runner', () => {
|
|
|
724
725
|
singletonServices
|
|
725
726
|
)
|
|
726
727
|
})
|
|
728
|
+
const wireFailingCommand = (error: unknown) => {
|
|
729
|
+
pikkuState(null, 'cli', 'meta', {
|
|
730
|
+
programs: {
|
|
731
|
+
'test-cli': {
|
|
732
|
+
program: 'test-cli',
|
|
733
|
+
commands: {
|
|
734
|
+
boom: {
|
|
735
|
+
command: 'boom',
|
|
736
|
+
pikkuFuncId: 'boomFunc',
|
|
737
|
+
positionals: [],
|
|
738
|
+
options: {},
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
options: {},
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
renderers: {},
|
|
745
|
+
})
|
|
746
|
+
pikkuState(null, 'cli', 'programs', {
|
|
747
|
+
'test-cli': {
|
|
748
|
+
defaultRenderer: undefined,
|
|
749
|
+
middleware: [],
|
|
750
|
+
renderers: {},
|
|
751
|
+
},
|
|
752
|
+
})
|
|
753
|
+
pikkuState(null, 'function', 'meta', {
|
|
754
|
+
boomFunc: {
|
|
755
|
+
pikkuFuncId: 'boomFunc',
|
|
756
|
+
inputSchemaName: null,
|
|
757
|
+
outputSchemaName: null,
|
|
758
|
+
sessionless: true,
|
|
759
|
+
},
|
|
760
|
+
})
|
|
761
|
+
addFunction('boomFunc', {
|
|
762
|
+
func: async () => {
|
|
763
|
+
throw error
|
|
764
|
+
},
|
|
765
|
+
auth: false,
|
|
766
|
+
})
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const captureStderr = async (run: () => Promise<void>) => {
|
|
770
|
+
const errors: string[] = []
|
|
771
|
+
const originalError = console.error
|
|
772
|
+
console.error = (message?: any) => {
|
|
773
|
+
errors.push(String(message))
|
|
774
|
+
}
|
|
775
|
+
try {
|
|
776
|
+
await assert.rejects(run, CLIError)
|
|
777
|
+
} finally {
|
|
778
|
+
console.error = originalError
|
|
779
|
+
}
|
|
780
|
+
return errors
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
test('should print an expected failure as its message alone', async () => {
|
|
784
|
+
wireFailingCommand(new PikkuError('Refusing to run — the seed drifted.'))
|
|
785
|
+
|
|
786
|
+
const errors = await captureStderr(() =>
|
|
787
|
+
executeCLI({
|
|
788
|
+
programName: 'test-cli',
|
|
789
|
+
args: ['boom'],
|
|
790
|
+
createSingletonServices: async () => singletonServices,
|
|
791
|
+
})
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
assert.deepStrictEqual(errors, ['Refusing to run — the seed drifted.'])
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
test('should keep the stack of an unexpected failure', async () => {
|
|
798
|
+
wireFailingCommand(new TypeError('cannot read x of undefined'))
|
|
799
|
+
|
|
800
|
+
const errors = await captureStderr(() =>
|
|
801
|
+
executeCLI({
|
|
802
|
+
programName: 'test-cli',
|
|
803
|
+
args: ['boom'],
|
|
804
|
+
createSingletonServices: async () => singletonServices,
|
|
805
|
+
})
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
assert.strictEqual(errors.length, 1)
|
|
809
|
+
assert.ok(errors[0]!.startsWith('TypeError: cannot read x of undefined'))
|
|
810
|
+
assert.ok(errors[0]!.includes('at '))
|
|
811
|
+
assert.ok(!errors[0]!.includes('Error: Error:'))
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
test('should add the stack of an expected failure when --verbose is passed', async () => {
|
|
815
|
+
wireFailingCommand(new PikkuError('Refusing to run.'))
|
|
816
|
+
|
|
817
|
+
const errors = await captureStderr(() =>
|
|
818
|
+
executeCLI({
|
|
819
|
+
programName: 'test-cli',
|
|
820
|
+
args: ['boom', '--verbose'],
|
|
821
|
+
createSingletonServices: async () => singletonServices,
|
|
822
|
+
})
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
// `--verbose` is not an option this command declares, so the parser
|
|
826
|
+
// warns about it first — the trace is whatever it printed last.
|
|
827
|
+
const printed = errors.at(-1)!
|
|
828
|
+
assert.ok(printed.startsWith('Refusing to run.\n'))
|
|
829
|
+
assert.ok(printed.includes('at '))
|
|
830
|
+
})
|
|
727
831
|
})
|
|
728
832
|
})
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { NotFoundError } from '../../errors/errors.js'
|
|
2
|
-
import { isExpectedError } from '../../errors/error-handler.js'
|
|
3
2
|
import { addFunction, runPikkuFunc } from '../../function/function-runner.js'
|
|
4
3
|
import { pikkuState } from '../../pikku-state.js'
|
|
5
4
|
import type { CoreUserSession } from '../../types/core.types.js'
|
|
@@ -30,6 +29,7 @@ import {
|
|
|
30
29
|
} from '../../services/user-session-service.js'
|
|
31
30
|
import { LocalVariablesService } from '../../services/local-variables.js'
|
|
32
31
|
import { generateCommandHelp, parseCLIArguments } from './command-parser.js'
|
|
32
|
+
import { formatCLIError, wantsStackTrace } from './format-cli-error.js'
|
|
33
33
|
|
|
34
34
|
/** The caller is expected to catch this and call `process.exit(exitCode)`. */
|
|
35
35
|
export class CLIError extends Error {
|
|
@@ -532,16 +532,7 @@ export async function executeCLI({
|
|
|
532
532
|
throw error
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
-
|
|
536
|
-
if (isExpectedError(error)) {
|
|
537
|
-
console.error(error.message)
|
|
538
|
-
} else {
|
|
539
|
-
console.error('Error:', error)
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
if (args.includes('--verbose') || args.includes('-v')) {
|
|
543
|
-
console.error('Stack trace:', error.stack)
|
|
544
|
-
}
|
|
535
|
+
console.error(formatCLIError(error, { verbose: wantsStackTrace(args) }))
|
|
545
536
|
|
|
546
537
|
throw new CLIError(error.message || String(error), 1)
|
|
547
538
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { test, describe } from 'node:test'
|
|
2
|
+
import * as assert from 'assert'
|
|
3
|
+
import { PikkuError } from '../../errors/error-handler.js'
|
|
4
|
+
import { formatCLIError, wantsStackTrace } from './format-cli-error.js'
|
|
5
|
+
|
|
6
|
+
describe('formatCLIError', () => {
|
|
7
|
+
test('prints an expected error as its message alone', () => {
|
|
8
|
+
const error = new PikkuError(
|
|
9
|
+
"Persona 'guest' missing guest. Refusing to run."
|
|
10
|
+
)
|
|
11
|
+
assert.strictEqual(
|
|
12
|
+
formatCLIError(error),
|
|
13
|
+
"Persona 'guest' missing guest. Refusing to run."
|
|
14
|
+
)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('prints an error flagged as expected as its message alone', () => {
|
|
18
|
+
const error = Object.assign(new Error('token expired'), { expected: true })
|
|
19
|
+
assert.strictEqual(formatCLIError(error), 'token expired')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('keeps the stack for an unexpected error', () => {
|
|
23
|
+
const error = new TypeError('cannot read properties of undefined')
|
|
24
|
+
const output = formatCLIError(error)
|
|
25
|
+
assert.ok(output.includes('TypeError: cannot read properties of undefined'))
|
|
26
|
+
assert.ok(output.includes('at '))
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('does not double the error name', () => {
|
|
30
|
+
const output = formatCLIError(new Error('boom'))
|
|
31
|
+
assert.ok(!output.includes('Error: Error:'))
|
|
32
|
+
assert.ok(output.startsWith('Error: boom'))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('adds the stack to an expected error when verbose', () => {
|
|
36
|
+
const error = new PikkuError('nope')
|
|
37
|
+
const output = formatCLIError(error, { verbose: true })
|
|
38
|
+
assert.ok(output.startsWith('nope\n'))
|
|
39
|
+
assert.ok(output.includes('at '))
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('summarises a fetch failure without inspecting the response', () => {
|
|
43
|
+
const error = Object.assign(new Error('Bad Gateway'), {
|
|
44
|
+
status: 502,
|
|
45
|
+
statusText: 'Bad Gateway',
|
|
46
|
+
response: {
|
|
47
|
+
url: 'https://api.pikkufabric.com/rpc/getDeploymentStatus',
|
|
48
|
+
headers: { forbidden: 'do not print me' },
|
|
49
|
+
body: 'a stream',
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
assert.strictEqual(
|
|
53
|
+
formatCLIError(error),
|
|
54
|
+
'502 Bad Gateway from https://api.pikkufabric.com/rpc/getDeploymentStatus'
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('keeps a fetch failure message that says more than the status', () => {
|
|
59
|
+
const error = Object.assign(new Error('Deployment not found'), {
|
|
60
|
+
status: 404,
|
|
61
|
+
statusText: 'Not Found',
|
|
62
|
+
response: { url: 'https://api.pikkufabric.com/rpc/getDeploymentStatus' },
|
|
63
|
+
})
|
|
64
|
+
assert.strictEqual(
|
|
65
|
+
formatCLIError(error),
|
|
66
|
+
'Deployment not found\n' +
|
|
67
|
+
' 404 Not Found from https://api.pikkufabric.com/rpc/getDeploymentStatus'
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('falls back to a string for a thrown non-error', () => {
|
|
72
|
+
assert.strictEqual(formatCLIError('just a string'), 'just a string')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('wantsStackTrace', () => {
|
|
77
|
+
test('is off by default', () => {
|
|
78
|
+
assert.strictEqual(wantsStackTrace(['deploy'], {}), false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('honours --verbose and -v', () => {
|
|
82
|
+
assert.strictEqual(wantsStackTrace(['deploy', '--verbose'], {}), true)
|
|
83
|
+
assert.strictEqual(wantsStackTrace(['deploy', '-v'], {}), true)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('honours PIKKU_DEBUG, but not when it is switched off', () => {
|
|
87
|
+
assert.strictEqual(wantsStackTrace(['deploy'], { PIKKU_DEBUG: '1' }), true)
|
|
88
|
+
assert.strictEqual(wantsStackTrace(['deploy'], { PIKKU_DEBUG: '0' }), false)
|
|
89
|
+
assert.strictEqual(wantsStackTrace(['deploy'], { PIKKU_DEBUG: '' }), false)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { isExpectedError } from '../../errors/error-handler.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A `PikkuFetchError` — recognised by shape, not by `instanceof`, because
|
|
5
|
+
* `@pikku/client-fetch` depends on core and core cannot depend back on it.
|
|
6
|
+
*/
|
|
7
|
+
type FetchFailure = {
|
|
8
|
+
status: number
|
|
9
|
+
statusText: string
|
|
10
|
+
response: { url?: string }
|
|
11
|
+
message?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const isFetchFailure = (error: unknown): error is FetchFailure => {
|
|
15
|
+
const candidate = error as Partial<FetchFailure> | null
|
|
16
|
+
return (
|
|
17
|
+
typeof candidate?.status === 'number' &&
|
|
18
|
+
typeof candidate?.statusText === 'string' &&
|
|
19
|
+
typeof candidate?.response === 'object' &&
|
|
20
|
+
candidate.response !== null
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The user asked to see the machinery. `--verbose`/`-v` is the flag the CLI
|
|
26
|
+
* already documents; `PIKKU_DEBUG` is for the times the flag cannot be typed —
|
|
27
|
+
* a command that does not declare the option, or a CLI run from a script.
|
|
28
|
+
*/
|
|
29
|
+
export const wantsStackTrace = (
|
|
30
|
+
args: string[],
|
|
31
|
+
env: Record<string, string | undefined> = process.env
|
|
32
|
+
): boolean =>
|
|
33
|
+
args.includes('--verbose') ||
|
|
34
|
+
args.includes('-v') ||
|
|
35
|
+
(env.PIKKU_DEBUG !== undefined &&
|
|
36
|
+
env.PIKKU_DEBUG !== '' &&
|
|
37
|
+
env.PIKKU_DEBUG !== '0')
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What the CLI prints when a command throws.
|
|
41
|
+
*
|
|
42
|
+
* A stack trace is an answer to "which line of pikku broke", and almost every
|
|
43
|
+
* failure a user actually hits is not that question: a missing role, an expired
|
|
44
|
+
* token, a gateway that is down. Those errors are written to be read, so the
|
|
45
|
+
* message alone is the whole output — an expected error is one deliberately
|
|
46
|
+
* raised as `PikkuError` (or carrying `expected: true`), and everything else
|
|
47
|
+
* keeps its stack, because an unexpected `TypeError` with its frames removed is
|
|
48
|
+
* a bug nobody can diagnose.
|
|
49
|
+
*
|
|
50
|
+
* A stack already begins with `Name: message`, so it is returned as-is: the
|
|
51
|
+
* `console.error('Error:', error)` this replaced produced the doubled
|
|
52
|
+
* `Error: Error: …` prefix that made even real traces look broken.
|
|
53
|
+
*/
|
|
54
|
+
export const formatCLIError = (
|
|
55
|
+
error: unknown,
|
|
56
|
+
{ verbose = false }: { verbose?: boolean } = {}
|
|
57
|
+
): string => {
|
|
58
|
+
if (isFetchFailure(error)) {
|
|
59
|
+
return formatFetchFailure(error, verbose)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const stack = (error as { stack?: unknown } | null)?.stack
|
|
63
|
+
const message = (error as { message?: unknown } | null)?.message
|
|
64
|
+
|
|
65
|
+
if (!isExpectedError(error)) {
|
|
66
|
+
return typeof stack === 'string' && stack ? stack : String(error)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const text = typeof message === 'string' && message ? message : String(error)
|
|
70
|
+
return verbose && typeof stack === 'string' && stack
|
|
71
|
+
? `${text}\n${stack}`
|
|
72
|
+
: text
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A failed HTTP call, as the line the user needs: which status, from which URL.
|
|
77
|
+
*
|
|
78
|
+
* Never the `Response` itself. Node inspects an error's own properties when it
|
|
79
|
+
* prints one, so a thrown fetch error used to dump the headers, the body stream
|
|
80
|
+
* and the redirect flags — pages of output whose only real content was the
|
|
81
|
+
* status code.
|
|
82
|
+
*/
|
|
83
|
+
const formatFetchFailure = (error: FetchFailure, verbose: boolean): string => {
|
|
84
|
+
const url = error.response?.url
|
|
85
|
+
const where = url ? ` from ${url}` : ''
|
|
86
|
+
const summary = `${error.status} ${error.statusText}${where}`
|
|
87
|
+
const message = error.message
|
|
88
|
+
const text =
|
|
89
|
+
message && message !== error.statusText
|
|
90
|
+
? `${message}\n ${summary}`
|
|
91
|
+
: summary
|
|
92
|
+
const stack = (error as { stack?: unknown }).stack
|
|
93
|
+
return verbose && typeof stack === 'string' && stack
|
|
94
|
+
? `${text}\n${stack}`
|
|
95
|
+
: text
|
|
96
|
+
}
|
|
@@ -77,7 +77,6 @@ export const VIRTUAL_USER_VARIABLES = {
|
|
|
77
77
|
* fallback for a run nobody handed a token to, which is what a schedule is.
|
|
78
78
|
*/
|
|
79
79
|
operatorToken: 'FABRIC_OPERATOR_TOKEN',
|
|
80
|
-
createMissing: 'PIKKU_PERSONA_CREATE_MISSING',
|
|
81
80
|
} as const
|
|
82
81
|
|
|
83
82
|
/**
|
|
@@ -560,9 +559,6 @@ export const executeVirtualUserRun = async ({
|
|
|
560
559
|
)
|
|
561
560
|
}
|
|
562
561
|
|
|
563
|
-
const createMissing =
|
|
564
|
-
String(await variables.get(VIRTUAL_USER_VARIABLES.createMissing)) ===
|
|
565
|
-
'true'
|
|
566
562
|
const model = await variables.get(VIRTUAL_USER_VARIABLES.model)
|
|
567
563
|
if (!model) {
|
|
568
564
|
throw new Error(
|
|
@@ -606,7 +602,6 @@ export const executeVirtualUserRun = async ({
|
|
|
606
602
|
? {
|
|
607
603
|
operator: {
|
|
608
604
|
token,
|
|
609
|
-
createMissing,
|
|
610
605
|
signInPath: signInPathFor(configuredSignInPath, 'fabric'),
|
|
611
606
|
},
|
|
612
607
|
}
|