Files
start-os/sdk/package/lib/test/inputSpecBuilder.test.ts
Aiden McClelland f2142f0bb3 add documentation for ai agents (#3115)
* add documentation for ai agents

* docs: consolidate CLAUDE.md and CONTRIBUTING.md, add style guidelines

- Refactor CLAUDE.md to reference CONTRIBUTING.md for build/test/format info
- Expand CONTRIBUTING.md with comprehensive build targets, env vars, and testing
- Add code style guidelines section with conventional commits
- Standardize SDK prettier config to use single quotes (matching web)
- Add project-level Claude Code settings to disable co-author attribution

* style(sdk): apply prettier with single quotes

Run prettier across sdk/base and sdk/package to apply the
standardized quote style (single quotes matching web).

* docs: add USER.md for per-developer TODO filtering

- Add agents/USER.md to .gitignore (contains user identifier)
- Document session startup flow in CLAUDE.md:
  - Create USER.md if missing, prompting for identifier
  - Filter TODOs by @username tags
  - Offer relevant TODOs on session start

* docs: add i18n documentation task to agent TODOs

* docs: document i18n ID patterns in core/

Add agents/i18n-patterns.md covering rust-i18n setup, translation file
format, t!() macro usage, key naming conventions, and locale selection.
Remove completed TODO item and add reference in CLAUDE.md.

* chore: clarify that all builds work on any OS with Docker
2026-02-06 00:10:16 +01:00

879 lines
23 KiB
TypeScript

import { testOutput } from './output.test'
import { InputSpec } from '../../../base/lib/actions/input/builder/inputSpec'
import { List } from '../../../base/lib/actions/input/builder/list'
import { Value } from '../../../base/lib/actions/input/builder/value'
import { Variants } from '../../../base/lib/actions/input/builder/variants'
import { ValueSpec } from '../../../base/lib/actions/input/inputSpecTypes'
import { setupManifest } from '../manifest/setupManifest'
import { StartSdk } from '../StartSdk'
describe('builder tests', () => {
test('text', async () => {
const bitcoinPropertiesBuilt: {
'peer-tor-address': ValueSpec
} = await InputSpec.of({
'peer-tor-address': Value.text({
name: 'Peer tor address',
description: 'The Tor address of the peer interface',
required: true,
default: null,
}),
})
.build({} as any)
.then((a) => a.spec)
expect(bitcoinPropertiesBuilt).toMatchObject({
'peer-tor-address': {
type: 'text',
description: 'The Tor address of the peer interface',
warning: null,
masked: false,
placeholder: null,
minLength: null,
maxLength: null,
patterns: [],
disabled: false,
inputmode: 'text',
name: 'Peer tor address',
required: true,
default: null,
},
})
})
})
describe('values', () => {
test('toggle', async () => {
const value = await Value.toggle({
name: 'Testing',
description: null,
warning: null,
default: false,
}).build({} as any)
const validator = value.validator
validator.unsafeCast(false)
testOutput<typeof validator._TYPE, boolean>()(null)
})
test('text', async () => {
const value = await Value.text({
name: 'Testing',
required: true,
default: null,
}).build({} as any)
const validator = value.validator
const rawIs = value.spec
validator.unsafeCast('test text')
expect(() => validator.unsafeCast(null)).toThrowError()
testOutput<typeof validator._TYPE, string>()(null)
})
test('text with default', async () => {
const value = await Value.text({
name: 'Testing',
required: true,
default: 'this is a default value',
}).build({} as any)
const validator = value.validator
const rawIs = value.spec
validator.unsafeCast('test text')
expect(() => validator.unsafeCast(null)).toThrowError()
testOutput<typeof validator._TYPE, string>()(null)
})
test('optional text', async () => {
const value = await Value.text({
name: 'Testing',
required: false,
default: null,
}).build({} as any)
const validator = value.validator
const rawIs = value.spec
validator.unsafeCast('test text')
validator.unsafeCast(null)
testOutput<typeof validator._TYPE, string | null>()(null)
})
test('color', async () => {
const value = await Value.color({
name: 'Testing',
required: false,
default: null,
description: null,
warning: null,
}).build({} as any)
const validator = value.validator
validator.unsafeCast('#000000')
testOutput<typeof validator._TYPE, string | null>()(null)
})
test('datetime', async () => {
const value = await Value.datetime({
name: 'Testing',
required: true,
default: null,
description: null,
warning: null,
inputmode: 'date',
min: null,
max: null,
}).build({} as any)
const validator = value.validator
validator.unsafeCast('2021-01-01')
testOutput<typeof validator._TYPE, string>()(null)
})
test('optional datetime', async () => {
const value = await Value.datetime({
name: 'Testing',
required: false,
default: null,
description: null,
warning: null,
inputmode: 'date',
min: null,
max: null,
}).build({} as any)
const validator = value.validator
validator.unsafeCast('2021-01-01')
testOutput<typeof validator._TYPE, string | null>()(null)
})
test('textarea', async () => {
const value = await Value.textarea({
name: 'Testing',
required: false,
default: null,
description: null,
warning: null,
minLength: null,
maxLength: null,
minRows: 3,
maxRows: 6,
placeholder: null,
}).build({} as any)
const validator = value.validator
validator.unsafeCast('test text')
testOutput<typeof validator._TYPE, string | null>()(null)
})
test('number', async () => {
const value = await Value.number({
name: 'Testing',
required: true,
default: null,
integer: false,
description: null,
warning: null,
min: null,
max: null,
step: null,
units: null,
placeholder: null,
}).build({} as any)
const validator = value.validator
validator.unsafeCast(2)
testOutput<typeof validator._TYPE, number>()(null)
})
test('optional number', async () => {
const value = await Value.number({
name: 'Testing',
required: false,
default: null,
integer: false,
description: null,
warning: null,
min: null,
max: null,
step: null,
units: null,
placeholder: null,
}).build({} as any)
const validator = value.validator
validator.unsafeCast(2)
testOutput<typeof validator._TYPE, number | null>()(null)
})
test('select', async () => {
const value = await Value.select({
name: 'Testing',
default: 'a',
values: {
a: 'A',
b: 'B',
},
description: null,
warning: null,
}).build({} as any)
const validator = value.validator
validator.unsafeCast('a')
validator.unsafeCast('b')
expect(() => validator.unsafeCast('c')).toThrowError()
testOutput<typeof validator._TYPE, 'a' | 'b'>()(null)
})
test('nullable select', async () => {
const value = await Value.select({
name: 'Testing',
default: 'a',
values: {
a: 'A',
b: 'B',
},
description: null,
warning: null,
}).build({} as any)
const validator = value.validator
validator.unsafeCast('a')
validator.unsafeCast('b')
testOutput<typeof validator._TYPE, 'a' | 'b'>()(null)
})
test('multiselect', async () => {
const value = await Value.multiselect({
name: 'Testing',
values: {
a: 'A',
b: 'B',
},
default: [],
description: null,
warning: null,
minLength: null,
maxLength: null,
}).build({} as any)
const validator = value.validator
validator.unsafeCast([])
validator.unsafeCast(['a', 'b'])
expect(() => validator.unsafeCast(['e'])).toThrowError()
expect(() => validator.unsafeCast([4])).toThrowError()
testOutput<typeof validator._TYPE, Array<'a' | 'b'>>()(null)
})
test('object', async () => {
const value = await Value.object(
{
name: 'Testing',
description: null,
},
InputSpec.of({
a: Value.toggle({
name: 'test',
description: null,
warning: null,
default: false,
}),
}),
).build({} as any)
const validator = value.validator
validator.unsafeCast({ a: true })
testOutput<typeof validator._TYPE, { a: boolean }>()(null)
})
test('union', async () => {
const value = await Value.union({
name: 'Testing',
default: 'a',
description: null,
warning: null,
variants: Variants.of({
a: {
name: 'a',
spec: InputSpec.of({
b: Value.toggle({
name: 'b',
description: null,
warning: null,
default: false,
}),
}),
},
}),
}).build({} as any)
const validator = value.validator
validator.unsafeCast({ selection: 'a', value: { b: false } })
type Test = typeof validator._TYPE
testOutput<
Test,
{
selection: 'a'
value: {
b: boolean
}
other?: {}
}
>()(null)
})
describe('dynamic', () => {
const fakeOptions = {
inputSpec: 'inputSpec',
effects: 'effects',
utils: 'utils',
} as any
test('toggle', async () => {
const value = await Value.dynamicToggle(async () => ({
name: 'Testing',
description: null,
warning: null,
default: false,
})).build({} as any)
const validator = value.validator
validator.unsafeCast(false)
expect(() => validator.unsafeCast(null)).toThrowError()
testOutput<typeof validator._TYPE, boolean>()(null)
expect(value.spec).toMatchObject({
name: 'Testing',
description: null,
warning: null,
default: false,
})
})
test('text', async () => {
const value = await Value.dynamicText(async () => ({
name: 'Testing',
required: false,
default: null,
})).build({} as any)
const validator = value.validator
const rawIs = value.spec
validator.unsafeCast('test text')
validator.unsafeCast(null)
testOutput<typeof validator._TYPE, string | null>()(null)
expect(value.spec).toMatchObject({
name: 'Testing',
required: false,
default: null,
})
})
test('text with default', async () => {
const value = await Value.dynamicText(async () => ({
name: 'Testing',
required: false,
default: 'this is a default value',
})).build({} as any)
const validator = value.validator
validator.unsafeCast('test text')
validator.unsafeCast(null)
testOutput<typeof validator._TYPE, string | null>()(null)
expect(value.spec).toMatchObject({
name: 'Testing',
required: false,
default: 'this is a default value',
})
})
test('optional text', async () => {
const value = await Value.dynamicText(async () => ({
name: 'Testing',
required: false,
default: null,
})).build({} as any)
const validator = value.validator
const rawIs = value.spec
validator.unsafeCast('test text')
validator.unsafeCast(null)
testOutput<typeof validator._TYPE, string | null>()(null)
expect(value.spec).toMatchObject({
name: 'Testing',
required: false,
default: null,
})
})
test('color', async () => {
const value = await Value.dynamicColor(async () => ({
name: 'Testing',
required: false,
default: null,
description: null,
warning: null,
})).build({} as any)
const validator = value.validator
validator.unsafeCast('#000000')
validator.unsafeCast(null)
testOutput<typeof validator._TYPE, string | null>()(null)
expect(value.spec).toMatchObject({
name: 'Testing',
required: false,
default: null,
description: null,
warning: null,
})
})
test('datetime', async () => {
const sdk = StartSdk.of()
.withManifest(
setupManifest({
id: 'testOutput',
title: '',
license: '',
wrapperRepo: '',
upstreamRepo: '',
supportSite: '',
marketingSite: '',
donationUrl: null,
docsUrl: '',
description: {
short: '',
long: '',
},
images: {},
volumes: [],
alerts: {
install: null,
update: null,
uninstall: null,
restore: null,
start: null,
stop: null,
},
dependencies: {
'remote-test': {
description: '',
optional: true,
s9pk: 'https://example.com/remote-test.s9pk',
},
},
}),
)
.build(true)
const value = await Value.dynamicDatetime(async ({ effects }) => {
return {
name: 'Testing',
required: false,
default: null,
inputmode: 'date',
}
}).build({} as any)
const validator = value.validator
validator.unsafeCast('2021-01-01')
validator.unsafeCast(null)
testOutput<typeof validator._TYPE, string | null>()(null)
expect(value.spec).toMatchObject({
name: 'Testing',
required: false,
default: null,
description: null,
warning: null,
inputmode: 'date',
})
})
test('textarea', async () => {
const value = await Value.dynamicTextarea(async () => ({
name: 'Testing',
required: false,
default: null,
description: null,
warning: null,
minLength: null,
maxLength: null,
minRows: 3,
maxRows: 6,
placeholder: null,
})).build({} as any)
const validator = value.validator
validator.unsafeCast('test text')
testOutput<typeof validator._TYPE, string | null>()(null)
expect(value.spec).toMatchObject({
name: 'Testing',
required: false,
})
})
test('number', async () => {
const value = await Value.dynamicNumber(() => ({
name: 'Testing',
required: false,
default: null,
integer: false,
description: null,
warning: null,
min: null,
max: null,
step: null,
units: null,
placeholder: null,
})).build({} as any)
const validator = value.validator
validator.unsafeCast(2)
validator.unsafeCast(null)
expect(() => validator.unsafeCast('null')).toThrowError()
testOutput<typeof validator._TYPE, number | null>()(null)
expect(value.spec).toMatchObject({
name: 'Testing',
required: false,
})
})
test('select', async () => {
const value = await Value.dynamicSelect(() => ({
name: 'Testing',
default: 'a',
values: {
a: 'A',
b: 'B',
},
description: null,
warning: null,
})).build({} as any)
const validator = value.validator
validator.unsafeCast('a')
validator.unsafeCast('b')
testOutput<typeof validator._TYPE, 'a' | 'b'>()(null)
expect(value.spec).toMatchObject({
name: 'Testing',
})
})
test('multiselect', async () => {
const value = await Value.dynamicMultiselect(() => ({
name: 'Testing',
values: {
a: 'A',
b: 'B',
},
default: [],
description: null,
warning: null,
minLength: null,
maxLength: null,
})).build({} as any)
const validator = value.validator
validator.unsafeCast([])
validator.unsafeCast(['a', 'b'])
expect(() => validator.unsafeCast([4])).toThrowError()
expect(() => validator.unsafeCast(null)).toThrowError()
testOutput<typeof validator._TYPE, Array<'a' | 'b'>>()(null)
expect(value.spec).toMatchObject({
name: 'Testing',
default: [],
})
})
})
describe('filtering', () => {
test('union', async () => {
const value = await Value.dynamicUnion(() => ({
name: 'Testing',
default: 'a',
description: null,
warning: null,
disabled: ['a', 'c'],
variants: Variants.of({
a: {
name: 'a',
spec: InputSpec.of({
b: Value.toggle({
name: 'b',
description: null,
warning: null,
default: false,
}),
}),
},
b: {
name: 'b',
spec: InputSpec.of({
b: Value.toggle({
name: 'b',
description: null,
warning: null,
default: false,
}),
}),
},
}),
})).build({} as any)
const validator = value.validator
validator.unsafeCast({ selection: 'a', value: { b: false } })
type Test = typeof validator._TYPE
testOutput<
Test,
| {
selection: 'a'
value: {
b: boolean
}
other?: {
b?: {
b?: boolean
}
}
}
| {
selection: 'b'
value: {
b: boolean
}
other?: {
a?: {
b?: boolean
}
}
}
>()(null)
const built = value.spec
expect(built).toMatchObject({
name: 'Testing',
variants: {
b: {},
},
})
expect(built).toMatchObject({
name: 'Testing',
variants: {
a: {},
b: {},
},
})
expect(built).toMatchObject({
name: 'Testing',
variants: {
a: {},
b: {},
},
disabled: ['a', 'c'],
})
})
})
test('dynamic union', async () => {
const value = await Value.dynamicUnion(() => ({
disabled: ['a', 'c'],
name: 'Testing',
default: 'b',
description: null,
warning: null,
variants: Variants.of({
a: {
name: 'a',
spec: InputSpec.of({
b: Value.toggle({
name: 'b',
description: null,
warning: null,
default: false,
}),
}),
},
b: {
name: 'b',
spec: InputSpec.of({
b: Value.toggle({
name: 'b',
description: null,
warning: null,
default: false,
}),
}),
},
}),
})).build({} as any)
const validator = value.validator
validator.unsafeCast({ selection: 'a', value: { b: false } })
type Test = typeof validator._TYPE
testOutput<
Test,
| {
selection: 'a'
value: {
b: boolean
}
other?: {
b?: {
b?: boolean
}
}
}
| {
selection: 'b'
value: {
b: boolean
}
other?: {
a?: {
b?: boolean
}
}
}
>()(null)
const built = value.spec
expect(built).toMatchObject({
name: 'Testing',
variants: {
b: {},
},
})
expect(built).toMatchObject({
name: 'Testing',
variants: {
a: {},
b: {},
},
})
expect(built).toMatchObject({
name: 'Testing',
variants: {
a: {},
b: {},
},
disabled: ['a', 'c'],
})
})
})
describe('Builder List', () => {
test('obj', async () => {
const value = await Value.list(
List.obj(
{
name: 'test',
},
{
spec: InputSpec.of({
test: Value.toggle({
name: 'test',
description: null,
warning: null,
default: false,
}),
}),
},
),
).build({} as any)
const validator = value.validator
validator.unsafeCast([{ test: true }])
testOutput<typeof validator._TYPE, { test: boolean }[]>()(null)
})
test('text', async () => {
const value = await Value.list(
List.text(
{
name: 'test',
},
{
patterns: [],
},
),
).build({} as any)
const validator = value.validator
validator.unsafeCast(['test', 'text'])
testOutput<typeof validator._TYPE, string[]>()(null)
})
describe('dynamic', () => {
test('text', async () => {
const value = await Value.list(
List.dynamicText(() => ({
name: 'test',
spec: { patterns: [] },
})),
).build({} as any)
const validator = value.validator
validator.unsafeCast(['test', 'text'])
expect(() => validator.unsafeCast([3, 4])).toThrowError()
expect(() => validator.unsafeCast(null)).toThrowError()
testOutput<typeof validator._TYPE, string[]>()(null)
expect(value.spec).toMatchObject({
name: 'test',
spec: { patterns: [] },
})
})
})
})
describe('Nested nullable values', () => {
test('Testing text', async () => {
const value = await InputSpec.of({
a: Value.text({
name: 'Temp Name',
description:
'If no name is provided, the name from inputSpec will be used',
required: false,
default: null,
}),
}).build({} as any)
const validator = value.validator
validator.unsafeCast({ a: null })
validator.unsafeCast({ a: 'test' })
expect(() => validator.unsafeCast({ a: 4 })).toThrowError()
testOutput<typeof validator._TYPE, { a: string | null }>()(null)
})
test('Testing number', async () => {
const value = await InputSpec.of({
a: Value.number({
name: 'Temp Name',
description:
'If no name is provided, the name from inputSpec will be used',
required: false,
default: null,
warning: null,
placeholder: null,
integer: false,
min: null,
max: null,
step: null,
units: null,
}),
}).build({} as any)
const validator = value.validator
validator.unsafeCast({ a: null })
validator.unsafeCast({ a: 5 })
expect(() => validator.unsafeCast({ a: '4' })).toThrowError()
testOutput<typeof validator._TYPE, { a: number | null }>()(null)
})
test('Testing color', async () => {
const value = await InputSpec.of({
a: Value.color({
name: 'Temp Name',
description:
'If no name is provided, the name from inputSpec will be used',
required: false,
default: null,
warning: null,
}),
}).build({} as any)
const validator = value.validator
validator.unsafeCast({ a: null })
validator.unsafeCast({ a: '5' })
expect(() => validator.unsafeCast({ a: 4 })).toThrowError()
testOutput<typeof validator._TYPE, { a: string | null }>()(null)
})
test('Testing select', async () => {
const value = await InputSpec.of({
a: Value.select({
name: 'Temp Name',
description:
'If no name is provided, the name from inputSpec will be used',
default: 'a',
warning: null,
values: {
a: 'A',
},
}),
}).build({} as any)
const higher = await Value.select({
name: 'Temp Name',
description:
'If no name is provided, the name from inputSpec will be used',
default: 'a',
warning: null,
values: {
a: 'A',
},
}).build({} as any)
const validator = value.validator
validator.unsafeCast({ a: 'a' })
expect(() => validator.unsafeCast({ a: '4' })).toThrowError()
testOutput<typeof validator._TYPE, { a: 'a' }>()(null)
})
test('Testing multiselect', async () => {
const value = await InputSpec.of({
a: Value.multiselect({
name: 'Temp Name',
description:
'If no name is provided, the name from inputSpec will be used',
warning: null,
default: [],
values: {
a: 'A',
},
minLength: null,
maxLength: null,
}),
}).build({} as any)
const validator = value.validator
validator.unsafeCast({ a: [] })
validator.unsafeCast({ a: ['a'] })
expect(() => validator.unsafeCast({ a: ['4'] })).toThrowError()
expect(() => validator.unsafeCast({ a: '4' })).toThrowError()
testOutput<typeof validator._TYPE, { a: 'a'[] }>()(null)
})
})