feat: wrap template commands with envelope, update docs (Phase 4)
Fixes #72
This commit is contained in:
@@ -88,30 +88,60 @@ Or use the CLI (see [CLI Reference](#cli-reference) and [`packages/cli-json-cas/
|
|||||||
|
|
||||||
## CLI Reference
|
## CLI Reference
|
||||||
|
|
||||||
Binary: `json-cas` (from `@uncaged/cli-json-cas`). Default store: `~/.uncaged/json-cas`.
|
Binary: `json-cas` (also aliased `ucas`, from `@uncaged/cli-json-cas`). Default store:
|
||||||
|
`~/.uncaged/json-cas`. The store is auto-created and bootstrapped on first use — there is
|
||||||
|
no `init`/`bootstrap` command, and schemas are ordinary `@schema`-typed nodes (`ucas put
|
||||||
|
@schema file.json`), so there is no `schema` subcommand.
|
||||||
|
|
||||||
|
### Envelope format
|
||||||
|
|
||||||
|
Every JSON-emitting command prints a uniform `{ type, value }` envelope. `type` is the hash
|
||||||
|
of the command's `@output/*` result schema and `value` is the command payload. This makes
|
||||||
|
output self-describing and pipeable: feed any envelope into `render -p` to render its
|
||||||
|
`value` (embedded `cas_ref` hashes are expanded). `render` is the only command that emits
|
||||||
|
raw (non-envelope) text.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// ucas has <hash>
|
||||||
|
{ "type": "AYHQD2YA9G667", "value": true }
|
||||||
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
Usage: json-cas [--store <path>] [--json] <command> [args]
|
Usage: json-cas [--store <path>] [--json] <command> [args]
|
||||||
|
|
||||||
Commands:
|
Commands (all emit a { type, value } envelope unless noted):
|
||||||
init Create store dir and write bootstrap seed
|
put <type-hash> <file.json> Store node (value = hash) (@output/put)
|
||||||
bootstrap Write meta-schema seed, print hash
|
get <hash> Node payload + metadata (@output/get)
|
||||||
schema put <file.json> Register schema, print type hash
|
has <hash> Existence boolean (@output/has)
|
||||||
schema get <type-hash> Print schema JSON
|
verify <hash> ok / corrupted / invalid (@output/verify)
|
||||||
schema list List all schemas (name + hash)
|
refs <hash> Direct cas_ref edges (@output/refs)
|
||||||
schema validate <hash> Validate node against its schema
|
walk <hash> [--format tree] Recursive traversal (@output/walk)
|
||||||
put <type-hash> <file.json> Store node, print hash
|
hash <type-hash> <file.json> Compute hash without storing (@output/hash)
|
||||||
get <hash> Print node as JSON
|
render <hash> [options] Render node as text (raw output)
|
||||||
has <hash> Print true/false
|
render --pipe/-p [options] Render a piped envelope (raw output)
|
||||||
verify <hash> Verify integrity, print ok/corrupted
|
list --type <hash-or-alias> Hashes for a type (value = list) (@output/list)
|
||||||
refs <hash> List direct cas_ref edges
|
var set|get|delete|tag|list ... Variable CRUD (@output/var-*)
|
||||||
walk <hash> [--format tree] Recursive traversal
|
template set|get|list|delete ... Output-template CRUD (@output/template-*)
|
||||||
hash <type-hash> <file.json> Compute hash without storing (dry run)
|
gc Garbage collection (@output/gc)
|
||||||
cat <hash> [--payload] Output node (--payload for payload only)
|
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--store <path> Store directory (default: ~/.uncaged/json-cas)
|
--store <path> Store directory (default: ~/.uncaged/json-cas)
|
||||||
--json Compact JSON output
|
--json Compact JSON output
|
||||||
|
--pipe, -p Read a { type, value } envelope from stdin for render
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pipe examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Store a node, then render the stored content (the put envelope's hash is
|
||||||
|
# a cas_ref, so render -p dereferences and renders it):
|
||||||
|
ucas put @schema ./schemas/item.json | ucas render -p
|
||||||
|
|
||||||
|
# Render garbage-collection stats:
|
||||||
|
ucas gc | ucas render -p
|
||||||
|
|
||||||
|
# List every schema, then consume the envelope's value array with jq:
|
||||||
|
ucas list --type @schema | jq -r '.value[]'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ CLI tool for json-cas stores.
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
`@uncaged/cli-json-cas` provides the `json-cas` command for managing a filesystem-backed store: bootstrap, schema registration, node CRUD, integrity checks, reference listing, and graph walks. It uses `@uncaged/json-cas-fs` for persistence and `@uncaged/json-cas` for core operations.
|
`@uncaged/cli-json-cas` provides the `json-cas` command (also aliased `ucas`) for managing a filesystem-backed store: node CRUD, integrity checks, reference listing, graph walks, variables, and output templates. It uses `@uncaged/json-cas-fs` for persistence and `@uncaged/json-cas` for core operations.
|
||||||
|
|
||||||
|
The store is **auto-created and bootstrapped** on first use, so there is no `init`/`bootstrap` command. Schemas are ordinary `@schema`-typed nodes — register one with `ucas put @schema file.json` and list them with `ucas list --type @schema`; there is no dedicated `schema` subcommand.
|
||||||
|
|
||||||
**Dependencies:** `@uncaged/json-cas`, `@uncaged/json-cas-fs`
|
**Dependencies:** `@uncaged/json-cas`, `@uncaged/json-cas-fs`
|
||||||
|
|
||||||
@@ -37,48 +39,95 @@ Usage: json-cas [--store <path>] [--json] <command> [args]
|
|||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `--store <path>` | Store directory (default: `~/.uncaged/json-cas`) |
|
| `--store <path>` | Store directory (default: `~/.uncaged/json-cas`) |
|
||||||
| `--json` | Compact JSON output for commands that print JSON |
|
| `--var-db <path>` | Variable database path (default: `<store>/variables.db`) |
|
||||||
|
| `--json` | Compact (single-line) JSON output |
|
||||||
|
|
||||||
|
### Envelope format
|
||||||
|
|
||||||
|
Every JSON-emitting command prints a uniform `{ type, value }` envelope. `type` is the hash
|
||||||
|
of the command's `@output/*` result schema and `value` is the command payload. The output
|
||||||
|
is therefore self-describing and pipeable: feed any envelope into `render -p` to render its
|
||||||
|
`value` (embedded `cas_ref` hashes are expanded). `render` is the only command that emits
|
||||||
|
raw, non-envelope text.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// json-cas has <hash>
|
||||||
|
{ "type": "AYHQD2YA9G667", "value": true }
|
||||||
|
|
||||||
|
// json-cas template set <schema-hash> --inline "Hi {{ payload.name }}"
|
||||||
|
{ "type": "9YJZ09DDAYAWR", "value": { "schemaHash": "7XX5H51CVD9H0", "contentHash": "FC8WACA792B6F" } }
|
||||||
|
```
|
||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Envelope `value` | Result schema |
|
||||||
|---------|-------------|
|
|---------|------------------|---------------|
|
||||||
| `init` | Create store directory and write bootstrap seed; prints meta hash |
|
| `put <type-hash> <file.json>` | stored node hash (string) | `@output/put` |
|
||||||
| `bootstrap` | Write meta-schema seed into existing store; prints hash |
|
| `get <hash>` | `{ type, payload, timestamp }` | `@output/get` |
|
||||||
| `schema put <file.json>` | Register schema from file; prints type hash |
|
| `has <hash>` | boolean | `@output/has` |
|
||||||
| `schema get <type-hash>` | Print schema JSON |
|
| `verify <hash>` | `ok` / `corrupted` / `invalid` | `@output/verify` |
|
||||||
| `schema list` | List all schemas (`hash name`) |
|
| `refs <hash>` | hashes (string[]) | `@output/refs` |
|
||||||
| `schema validate <hash>` | Validate node against its schema; prints `valid` / `invalid` |
|
| `walk <hash> [--format tree]` | hashes (string[]) or tree string | `@output/walk` |
|
||||||
| `put <type-hash> <file.json>` | Store node; prints content hash |
|
| `hash <type-hash> <file.json>` | computed hash (string) | `@output/hash` |
|
||||||
| `get <hash>` | Print full node as JSON |
|
| `render <hash> [options]` | raw text (no envelope) | — |
|
||||||
| `has <hash>` | Print `true` or `false` |
|
| `render --pipe/-p [options]` | raw text from piped envelope | — |
|
||||||
| `verify <hash>` | Verify integrity; prints `ok` or `corrupted` |
|
| `list --type <hash-or-alias>` | hashes (string[]) | `@output/list` |
|
||||||
| `refs <hash>` | Print direct `cas_ref` targets (one per line) |
|
| `var set <name> <hash> [--tag ...]` | variable object | `@output/var-set` |
|
||||||
| `walk <hash>` | BFS traversal; one hash per line |
|
| `var get <name> --schema <hash>` | variable object | `@output/var-get` |
|
||||||
| `walk <hash> --format tree` | Tree-formatted traversal |
|
| `var delete <name> [--schema <hash>]` | variable or variable[] | `@output/var-delete` |
|
||||||
| `hash <type-hash> <file.json>` | Compute hash without storing |
|
| `var tag <name> --schema <hash> <ops...>` | variable object | `@output/var-tag` |
|
||||||
| `cat <hash>` | Print node JSON |
|
| `var list [prefix] [--schema <hash>] [--tag ...]` | variable[] | `@output/var-list` |
|
||||||
| `cat <hash> --payload` | Print payload only |
|
| `template set <schema-hash> <file> \| --inline <text>` | `{ schemaHash, contentHash }` | `@output/template-set` |
|
||||||
|
| `template get <schema-hash>` | template content (string) | `@output/template-get` |
|
||||||
|
| `template list` | `{ schemaHash, contentHash }[]` | `@output/template-list` |
|
||||||
|
| `template delete <schema-hash>` | `{ deleted: boolean }` | `@output/template-delete` |
|
||||||
|
| `gc` | `{ total, reachable, collected, scanned }` | `@output/gc` |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Initialize default store at ~/.uncaged/json-cas
|
# Register a schema (schemas are plain @schema nodes) and store a payload
|
||||||
json-cas init
|
json-cas put @schema ./schemas/item.json
|
||||||
|
# → { "type": "...", "value": "0123456789ABC" } (the schema's type hash)
|
||||||
|
|
||||||
# Use a custom store path
|
json-cas put 0123456789ABC ./payloads/item.json
|
||||||
json-cas --store ./data/cas bootstrap
|
# → { "type": "...", "value": "<content-hash>" }
|
||||||
|
|
||||||
# Register a schema and store a payload
|
|
||||||
json-cas schema put ./schemas/item.json
|
|
||||||
# → prints type hash, e.g. 0123456789ABCD
|
|
||||||
|
|
||||||
json-cas put 0123456789ABCD ./payloads/item.json
|
|
||||||
# → prints content hash
|
|
||||||
|
|
||||||
json-cas get <content-hash> --json
|
json-cas get <content-hash> --json
|
||||||
json-cas verify <content-hash>
|
json-cas verify <content-hash>
|
||||||
json-cas walk <content-hash> --format tree
|
json-cas walk <content-hash> --format tree
|
||||||
|
|
||||||
|
# List every registered schema, then extract the hashes with jq
|
||||||
|
json-cas list --type @schema | jq -r '.value[]'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pipe composition
|
||||||
|
|
||||||
|
Because every command shares the `{ type, value }` envelope, output composes directly into
|
||||||
|
`render -p`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# put emits a cas_ref hash envelope; render -p dereferences and renders the node
|
||||||
|
json-cas put @schema ./schemas/item.json | json-cas render -p
|
||||||
|
|
||||||
|
# render gc statistics
|
||||||
|
json-cas gc | json-cas render -p
|
||||||
|
|
||||||
|
# render every schema referenced by a list result
|
||||||
|
json-cas list --type @schema | json-cas render -p
|
||||||
|
```
|
||||||
|
|
||||||
|
### Templates
|
||||||
|
|
||||||
|
`template` commands manage the LiquidJS template bound to a schema (stored as a
|
||||||
|
`@ucas/template/text/<schema-hash>` variable). `render <hash>` uses the template registered
|
||||||
|
for the node's type, falling back to YAML when none exists.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Bind a template to a schema, then render a node of that type
|
||||||
|
json-cas template set 0123456789ABC --inline "Item: {{ payload.name }}"
|
||||||
|
json-cas render <content-hash>
|
||||||
|
# → Item: Widget
|
||||||
```
|
```
|
||||||
|
|
||||||
## Internal Structure
|
## Internal Structure
|
||||||
|
|||||||
@@ -197,27 +197,41 @@ exports[`Phase 3: Variable System 3.10 var delete removes variable 1`] = `
|
|||||||
exports[`Phase 3: Variable System 3.11 var get deleted variable returns not found 1`] = `"Error: Variable not found: name=myapp/config, schema=7XX5H51CVD9H0"`;
|
exports[`Phase 3: Variable System 3.11 var get deleted variable returns not found 1`] = `"Error: Variable not found: name=myapp/config, schema=7XX5H51CVD9H0"`;
|
||||||
|
|
||||||
exports[`Phase 4: Template System 4.1 template set registers template 1`] = `
|
exports[`Phase 4: Template System 4.1 template set registers template 1`] = `
|
||||||
"{
|
{
|
||||||
"schemaHash": "7XX5H51CVD9H0",
|
"type": "9YJZ09DDAYAWR",
|
||||||
"contentHash": "FC8WACA792B6F"
|
"value": {
|
||||||
}"
|
"contentHash": "FC8WACA792B6F",
|
||||||
|
"schemaHash": "7XX5H51CVD9H0",
|
||||||
|
},
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Phase 4: Template System 4.2 template get returns template text 1`] = `"Name: {{ payload.name }}, Age: {{ payload.age }}"`;
|
exports[`Phase 4: Template System 4.2 template get returns template text 1`] = `
|
||||||
|
{
|
||||||
|
"type": "FJG23DR9456WA",
|
||||||
|
"value": "Name: {{ payload.name }}, Age: {{ payload.age }}",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Phase 4: Template System 4.3 template list shows registered templates 1`] = `
|
exports[`Phase 4: Template System 4.3 template list shows registered templates 1`] = `
|
||||||
"[
|
{
|
||||||
{
|
"type": "3JB2JHXHZG2Z1",
|
||||||
"schemaHash": "7XX5H51CVD9H0",
|
"value": [
|
||||||
"preview": "Name: {{ payload.name }}, Age: {{ payload.age }}"
|
{
|
||||||
}
|
"contentHash": "FC8WACA792B6F",
|
||||||
]"
|
"schemaHash": "7XX5H51CVD9H0",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Phase 4: Template System 4.4 template delete removes template 1`] = `
|
exports[`Phase 4: Template System 4.4 template delete removes template 1`] = `
|
||||||
"{
|
{
|
||||||
"deleted": true
|
"type": "0PYGQE16XPM70",
|
||||||
}"
|
"value": {
|
||||||
|
"deleted": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Phase 4: Template System 4.5 template get deleted template returns not found 1`] = `"Error: Template not found for schema: 7XX5H51CVD9H0"`;
|
exports[`Phase 4: Template System 4.5 template get deleted template returns not found 1`] = `"Error: Template not found for schema: 7XX5H51CVD9H0"`;
|
||||||
@@ -235,27 +249,31 @@ exports[`Phase 7: Edge Cases 7.4 var set name with invalid chars errors 1`] = `"
|
|||||||
exports[`Phase 7: Edge Cases 7.5 no subcommand shows help text 1`] = `
|
exports[`Phase 7: Edge Cases 7.5 no subcommand shows help text 1`] = `
|
||||||
"Usage: json-cas [--store <path>] [--json] <command> [args]
|
"Usage: json-cas [--store <path>] [--json] <command> [args]
|
||||||
|
|
||||||
|
All JSON commands emit a { type, value } envelope. The type is the hash of the
|
||||||
|
command's @output/* schema (shown in parentheses); pipe any envelope into
|
||||||
|
\`render -p\` to render its value (cas_ref hashes are expanded).
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
put <type-hash> <file.json> Store node, print { type, value } envelope (value=hash)
|
put <type-hash> <file.json> Store node, print envelope (value=hash) (@output/put)
|
||||||
get <hash> Print node as { type, value } envelope
|
get <hash> Print node as envelope (@output/get)
|
||||||
has <hash> Print { type, value } envelope (value=boolean)
|
has <hash> Print envelope (value=boolean) (@output/has)
|
||||||
verify <hash> Verify integrity + schema → { type, value } (value=ok/corrupted/invalid)
|
verify <hash> Verify integrity + schema (value=ok/corrupted/invalid) (@output/verify)
|
||||||
refs <hash> List direct cas_ref edges
|
refs <hash> List direct cas_ref edges (@output/refs)
|
||||||
walk <hash> [--format tree] Recursive traversal
|
walk <hash> [--format tree] Recursive traversal (@output/walk)
|
||||||
hash <type-hash> <file.json> Compute hash without storing → { type, value } envelope
|
hash <type-hash> <file.json> Compute hash without storing (@output/hash)
|
||||||
render <hash> [options] Render node as YAML with resolution decay
|
render <hash> [options] Render node as text with resolution decay (raw output)
|
||||||
render --pipe/-p [options] Render { type, value } from stdin
|
render --pipe/-p [options] Render { type, value } from stdin (raw output)
|
||||||
list --type <hash-or-alias> List hashes for a type → { type, value } envelope (value=string[])
|
list --type <hash-or-alias> List hashes for a type (value=string[]) (@output/list)
|
||||||
var set <name> <hash> [--tag <tag>...] Create/update a variable
|
var set <name> <hash> [--tag <tag>...] Create/update a variable (@output/var-set)
|
||||||
var get <name> --schema <hash> Get a variable by name + schema
|
var get <name> --schema <hash> Get a variable by name + schema (@output/var-get)
|
||||||
var delete <name> [--schema <hash>] Delete variable(s)
|
var delete <name> [--schema <hash>] Delete variable(s) (@output/var-delete)
|
||||||
var list [prefix] [--schema <hash>] [--tag <tag>...] List variables
|
var list [prefix] [--schema <hash>] [--tag <tag>...] List variables (@output/var-list)
|
||||||
var tag <name> --schema <hash> <operations...> Modify tags/labels
|
var tag <name> --schema <hash> <operations...> Modify tags/labels (@output/var-tag)
|
||||||
template set <schema-hash> <file> | --inline <text> Set template for schema
|
template set <schema-hash> <file> | --inline <text> Set template for schema (@output/template-set)
|
||||||
template get <schema-hash> Get template content as raw text
|
template get <schema-hash> Get template content (value=string) (@output/template-get)
|
||||||
template list List all templates
|
template list List all templates (@output/template-list)
|
||||||
template delete <schema-hash> Delete template for schema
|
template delete <schema-hash> Delete template for schema (@output/template-delete)
|
||||||
gc Run garbage collection
|
gc Run garbage collection (@output/gc)
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--store <path> Store directory (default: ~/.uncaged/json-cas)
|
--store <path> Store directory (default: ~/.uncaged/json-cas)
|
||||||
|
|||||||
@@ -59,6 +59,23 @@ function envValue(json: string): unknown {
|
|||||||
return (JSON.parse(json) as { value: unknown }).value;
|
return (JSON.parse(json) as { value: unknown }).value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Run a CLI command feeding `stdin` to its standard input. */
|
||||||
|
async function runCliWithStdin(
|
||||||
|
args: string[],
|
||||||
|
stdin: string,
|
||||||
|
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
||||||
|
const proc = Bun.spawn(
|
||||||
|
["bun", entrypoint, "--store", tmpStore, "--var-db", varDbPath, ...args],
|
||||||
|
{ stdin: "pipe", stdout: "pipe", stderr: "pipe" },
|
||||||
|
);
|
||||||
|
proc.stdin.write(stdin);
|
||||||
|
proc.stdin.end();
|
||||||
|
const exitCode = await proc.exited;
|
||||||
|
const stdout = (await new Response(proc.stdout).text()).trim();
|
||||||
|
const stderr = (await new Response(proc.stderr).text()).trim();
|
||||||
|
return { stdout, stderr, exitCode };
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Phase 1: CAS Core ----
|
// ---- Phase 1: CAS Core ----
|
||||||
|
|
||||||
describe("Phase 1: CAS Core", () => {
|
describe("Phase 1: CAS Core", () => {
|
||||||
@@ -336,27 +353,29 @@ describe("Phase 4: Template System", () => {
|
|||||||
tmplFile,
|
tmplFile,
|
||||||
]);
|
]);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout).toMatchSnapshot();
|
expect(stripVolatile(stdout)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("4.2 template get returns template text", async () => {
|
test("4.2 template get returns template text", async () => {
|
||||||
const { stdout, exitCode } = await runCli(["template", "get", typeHash]);
|
const { stdout, exitCode } = await runCli(["template", "get", typeHash]);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout).toBe("Name: {{ payload.name }}, Age: {{ payload.age }}");
|
expect(envValue(stdout)).toBe(
|
||||||
expect(stdout).toMatchSnapshot();
|
"Name: {{ payload.name }}, Age: {{ payload.age }}",
|
||||||
|
);
|
||||||
|
expect(stripVolatile(stdout)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("4.3 template list shows registered templates", async () => {
|
test("4.3 template list shows registered templates", async () => {
|
||||||
const { stdout, exitCode } = await runCli(["template", "list"]);
|
const { stdout, exitCode } = await runCli(["template", "list"]);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout).toContain(typeHash);
|
expect(stdout).toContain(typeHash);
|
||||||
expect(stdout).toMatchSnapshot();
|
expect(stripVolatile(stdout)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("4.4 template delete removes template", async () => {
|
test("4.4 template delete removes template", async () => {
|
||||||
const { exitCode, stdout } = await runCli(["template", "delete", typeHash]);
|
const { exitCode, stdout } = await runCli(["template", "delete", typeHash]);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stdout).toMatchSnapshot();
|
expect(stripVolatile(stdout)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("4.5 template get deleted template returns not found", async () => {
|
test("4.5 template get deleted template returns not found", async () => {
|
||||||
@@ -520,3 +539,92 @@ describe("Phase 7: Edge Cases", () => {
|
|||||||
expect(stderr).toContain("not a directory");
|
expect(stderr).toContain("not a directory");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- Phase 8: Pipe Composition ----
|
||||||
|
//
|
||||||
|
// Every JSON command emits a { type, value } envelope, so its stdout can be
|
||||||
|
// fed straight into `render --pipe` (which renders the envelope value) or into
|
||||||
|
// any downstream JSON consumer. These tests verify the envelopes compose
|
||||||
|
// end-to-end.
|
||||||
|
|
||||||
|
describe("Phase 8: Pipe Composition", () => {
|
||||||
|
test("8.1 put | render -p expands the stored hash to its content", async () => {
|
||||||
|
const nodeFile = join(tmpStore, "pipe-node.json");
|
||||||
|
writeFileSync(nodeFile, JSON.stringify({ name: "Bob", age: 42 }));
|
||||||
|
|
||||||
|
const { stdout: putOut, exitCode: putExit } = await runCli([
|
||||||
|
"put",
|
||||||
|
typeHash,
|
||||||
|
nodeFile,
|
||||||
|
]);
|
||||||
|
expect(putExit).toBe(0);
|
||||||
|
|
||||||
|
// The put envelope value is a cas_ref hash; render -p dereferences it and
|
||||||
|
// renders the stored node's payload.
|
||||||
|
const { stdout, exitCode } = await runCliWithStdin(
|
||||||
|
["render", "--pipe"],
|
||||||
|
putOut,
|
||||||
|
);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toContain("Bob");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("8.2 gc | render -p renders the gc stats", async () => {
|
||||||
|
const { stdout: gcOut, exitCode: gcExit } = await runCli(["gc"]);
|
||||||
|
expect(gcExit).toBe(0);
|
||||||
|
|
||||||
|
const { stdout, exitCode } = await runCliWithStdin(
|
||||||
|
["render", "--pipe"],
|
||||||
|
gcOut,
|
||||||
|
);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
// gc value is an object { total, reachable, collected, scanned }
|
||||||
|
expect(stdout).toContain("total:");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("8.3 list --type @schema emits a parseable envelope of hashes", async () => {
|
||||||
|
const { stdout, exitCode } = await runCli(["list", "--type", "@schema"]);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
|
// Downstream consumers (jq, etc.) read the `value` array of hashes.
|
||||||
|
const value = envValue(stdout) as string[];
|
||||||
|
expect(Array.isArray(value)).toBe(true);
|
||||||
|
for (const hash of value) {
|
||||||
|
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("8.4 list --type @schema | render -p expands the schema list", async () => {
|
||||||
|
const { stdout: listOut } = await runCli(["list", "--type", "@schema"]);
|
||||||
|
// list result items are cas_ref hashes; render -p dereferences each one
|
||||||
|
// and renders the schema contents.
|
||||||
|
const { stdout, exitCode } = await runCliWithStdin(
|
||||||
|
["render", "--pipe"],
|
||||||
|
listOut,
|
||||||
|
);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("8.5 render <hash> uses a registered template", async () => {
|
||||||
|
// Register a template for the schema, then render a fresh node by hash.
|
||||||
|
const tmplFile = join(tmpStore, "pipe-render.liquid");
|
||||||
|
writeFileSync(tmplFile, "Person: {{ payload.name }} ({{ payload.age }})");
|
||||||
|
const { exitCode: setExit } = await runCli([
|
||||||
|
"template",
|
||||||
|
"set",
|
||||||
|
typeHash,
|
||||||
|
tmplFile,
|
||||||
|
]);
|
||||||
|
expect(setExit).toBe(0);
|
||||||
|
|
||||||
|
const nodeFile = join(tmpStore, "pipe-render-node.json");
|
||||||
|
writeFileSync(nodeFile, JSON.stringify({ name: "Carol", age: 25 }));
|
||||||
|
const { stdout: putOut } = await runCli(["put", typeHash, nodeFile]);
|
||||||
|
const freshHash = envValue(putOut) as string;
|
||||||
|
|
||||||
|
const { stdout, exitCode } = await runCli(["render", freshHash]);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe("Person: Carol (25)");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -656,10 +656,12 @@ async function cmdTemplateSet(args: string[]): Promise<void> {
|
|||||||
const varName = `@ucas/template/text/${schemaHash}`;
|
const varName = `@ucas/template/text/${schemaHash}`;
|
||||||
varStore.set(varName, contentHash);
|
varStore.set(varName, contentHash);
|
||||||
|
|
||||||
out({
|
out(
|
||||||
schemaHash,
|
await wrapEnvelope(store, "@output/template-set", {
|
||||||
contentHash,
|
schemaHash,
|
||||||
});
|
contentHash,
|
||||||
|
}),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof CasNodeNotFoundError) {
|
if (e instanceof CasNodeNotFoundError) {
|
||||||
die(`Error: ${e.message}`);
|
die(`Error: ${e.message}`);
|
||||||
@@ -695,8 +697,9 @@ async function cmdTemplateGet(args: string[]): Promise<void> {
|
|||||||
die(`Error: Content not found in CAS: ${variable.value}`);
|
die(`Error: Content not found in CAS: ${variable.value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output raw text (not JSON)
|
out(
|
||||||
process.stdout.write(node.payload as string);
|
await wrapEnvelope(store, "@output/template-get", node.payload as string),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
varStore.close();
|
varStore.close();
|
||||||
}
|
}
|
||||||
@@ -713,24 +716,12 @@ async function cmdTemplateList(_args: string[]): Promise<void> {
|
|||||||
schema: stringHash,
|
schema: stringHash,
|
||||||
});
|
});
|
||||||
|
|
||||||
const templates = variables.map((v) => {
|
const templates = variables.map((v) => ({
|
||||||
const schemaHash = v.name.replace("@ucas/template/text/", "");
|
schemaHash: v.name.replace("@ucas/template/text/", ""),
|
||||||
|
contentHash: v.value,
|
||||||
|
}));
|
||||||
|
|
||||||
// Get content for preview
|
out(await wrapEnvelope(store, "@output/template-list", templates));
|
||||||
const node = store.get(v.value);
|
|
||||||
const content = (node?.payload as string | undefined) ?? "";
|
|
||||||
|
|
||||||
// Truncate preview to 80 chars
|
|
||||||
const preview =
|
|
||||||
content.length > 80 ? `${content.slice(0, 77)}...` : content;
|
|
||||||
|
|
||||||
return {
|
|
||||||
schemaHash,
|
|
||||||
preview,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
out(templates);
|
|
||||||
} finally {
|
} finally {
|
||||||
varStore.close();
|
varStore.close();
|
||||||
}
|
}
|
||||||
@@ -751,7 +742,9 @@ async function cmdTemplateDelete(args: string[]): Promise<void> {
|
|||||||
const stringHash = await resolveTypeHash("@string");
|
const stringHash = await resolveTypeHash("@string");
|
||||||
varStore.remove(varName, stringHash);
|
varStore.remove(varName, stringHash);
|
||||||
|
|
||||||
out({ deleted: true });
|
out(
|
||||||
|
await wrapEnvelope(store, "@output/template-delete", { deleted: true }),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof VariableNotFoundError) {
|
if (e instanceof VariableNotFoundError) {
|
||||||
die(`Error: Template not found for schema: ${schemaHash}`);
|
die(`Error: Template not found for schema: ${schemaHash}`);
|
||||||
@@ -788,27 +781,31 @@ function printUsage(): void {
|
|||||||
console.log(`\
|
console.log(`\
|
||||||
Usage: json-cas [--store <path>] [--json] <command> [args]
|
Usage: json-cas [--store <path>] [--json] <command> [args]
|
||||||
|
|
||||||
|
All JSON commands emit a { type, value } envelope. The type is the hash of the
|
||||||
|
command's @output/* schema (shown in parentheses); pipe any envelope into
|
||||||
|
\`render -p\` to render its value (cas_ref hashes are expanded).
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
put <type-hash> <file.json> Store node, print { type, value } envelope (value=hash)
|
put <type-hash> <file.json> Store node, print envelope (value=hash) (@output/put)
|
||||||
get <hash> Print node as { type, value } envelope
|
get <hash> Print node as envelope (@output/get)
|
||||||
has <hash> Print { type, value } envelope (value=boolean)
|
has <hash> Print envelope (value=boolean) (@output/has)
|
||||||
verify <hash> Verify integrity + schema → { type, value } (value=ok/corrupted/invalid)
|
verify <hash> Verify integrity + schema (value=ok/corrupted/invalid) (@output/verify)
|
||||||
refs <hash> List direct cas_ref edges
|
refs <hash> List direct cas_ref edges (@output/refs)
|
||||||
walk <hash> [--format tree] Recursive traversal
|
walk <hash> [--format tree] Recursive traversal (@output/walk)
|
||||||
hash <type-hash> <file.json> Compute hash without storing → { type, value } envelope
|
hash <type-hash> <file.json> Compute hash without storing (@output/hash)
|
||||||
render <hash> [options] Render node as YAML with resolution decay
|
render <hash> [options] Render node as text with resolution decay (raw output)
|
||||||
render --pipe/-p [options] Render { type, value } from stdin
|
render --pipe/-p [options] Render { type, value } from stdin (raw output)
|
||||||
list --type <hash-or-alias> List hashes for a type → { type, value } envelope (value=string[])
|
list --type <hash-or-alias> List hashes for a type (value=string[]) (@output/list)
|
||||||
var set <name> <hash> [--tag <tag>...] Create/update a variable
|
var set <name> <hash> [--tag <tag>...] Create/update a variable (@output/var-set)
|
||||||
var get <name> --schema <hash> Get a variable by name + schema
|
var get <name> --schema <hash> Get a variable by name + schema (@output/var-get)
|
||||||
var delete <name> [--schema <hash>] Delete variable(s)
|
var delete <name> [--schema <hash>] Delete variable(s) (@output/var-delete)
|
||||||
var list [prefix] [--schema <hash>] [--tag <tag>...] List variables
|
var list [prefix] [--schema <hash>] [--tag <tag>...] List variables (@output/var-list)
|
||||||
var tag <name> --schema <hash> <operations...> Modify tags/labels
|
var tag <name> --schema <hash> <operations...> Modify tags/labels (@output/var-tag)
|
||||||
template set <schema-hash> <file> | --inline <text> Set template for schema
|
template set <schema-hash> <file> | --inline <text> Set template for schema (@output/template-set)
|
||||||
template get <schema-hash> Get template content as raw text
|
template get <schema-hash> Get template content (value=string) (@output/template-get)
|
||||||
template list List all templates
|
template list List all templates (@output/template-list)
|
||||||
template delete <schema-hash> Delete template for schema
|
template delete <schema-hash> Delete template for schema (@output/template-delete)
|
||||||
gc Run garbage collection
|
gc Run garbage collection (@output/gc)
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--store <path> Store directory (default: ~/.uncaged/json-cas)
|
--store <path> Store directory (default: ~/.uncaged/json-cas)
|
||||||
|
|||||||
@@ -103,9 +103,10 @@ describe("template set", () => {
|
|||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stderr).toBe("");
|
expect(stderr).toBe("");
|
||||||
|
|
||||||
const output = JSON.parse(stdout);
|
const envelope = JSON.parse(stdout);
|
||||||
expect(output).toHaveProperty("contentHash");
|
expect(envelope).toHaveProperty("type");
|
||||||
expect(output.schemaHash).toBe(stringHash);
|
expect(envelope.value).toHaveProperty("contentHash");
|
||||||
|
expect(envelope.value.schemaHash).toBe(stringHash);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("set template with --inline flag", async () => {
|
test("set template with --inline flag", async () => {
|
||||||
@@ -122,9 +123,10 @@ describe("template set", () => {
|
|||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const output = JSON.parse(stdout);
|
const envelope = JSON.parse(stdout);
|
||||||
expect(output).toHaveProperty("contentHash");
|
expect(envelope).toHaveProperty("type");
|
||||||
expect(output.schemaHash).toBe(stringHash);
|
expect(envelope.value).toHaveProperty("contentHash");
|
||||||
|
expect(envelope.value.schemaHash).toBe(stringHash);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("update existing template (idempotent)", async () => {
|
test("update existing template (idempotent)", async () => {
|
||||||
@@ -148,12 +150,12 @@ describe("template set", () => {
|
|||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const output = JSON.parse(stdout);
|
const envelope = JSON.parse(stdout);
|
||||||
expect(output).toHaveProperty("contentHash");
|
expect(envelope.value).toHaveProperty("contentHash");
|
||||||
|
|
||||||
// Verify we can get the new version
|
// Verify we can get the new version
|
||||||
const { stdout: getOut } = await runCli("template", "get", stringHash);
|
const { stdout: getOut } = await runCli("template", "get", stringHash);
|
||||||
expect(getOut).toBe("Version 2");
|
expect(JSON.parse(getOut).value).toBe("Version 2");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("error when file not found", async () => {
|
test("error when file not found", async () => {
|
||||||
@@ -223,7 +225,7 @@ describe("template set", () => {
|
|||||||
|
|
||||||
// Verify content
|
// Verify content
|
||||||
const { stdout: getOut } = await runCli("template", "get", stringHash);
|
const { stdout: getOut } = await runCli("template", "get", stringHash);
|
||||||
expect(getOut).toBe(multilineContent);
|
expect(JSON.parse(getOut).value).toBe(multilineContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("support empty templates", async () => {
|
test("support empty templates", async () => {
|
||||||
@@ -240,8 +242,8 @@ describe("template set", () => {
|
|||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const output = JSON.parse(stdout);
|
const envelope = JSON.parse(stdout);
|
||||||
expect(output).toHaveProperty("contentHash");
|
expect(envelope.value).toHaveProperty("contentHash");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("error when neither file nor --inline provided", async () => {
|
test("error when neither file nor --inline provided", async () => {
|
||||||
@@ -271,12 +273,12 @@ describe("template set", () => {
|
|||||||
|
|
||||||
// Verify content preserved
|
// Verify content preserved
|
||||||
const { stdout: getOut } = await runCli("template", "get", stringHash);
|
const { stdout: getOut } = await runCli("template", "get", stringHash);
|
||||||
expect(getOut).toBe(specialContent);
|
expect(JSON.parse(getOut).value).toBe(specialContent);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("template get", () => {
|
describe("template get", () => {
|
||||||
test("retrieve template as raw text", async () => {
|
test("retrieve template as envelope value", async () => {
|
||||||
const store = createFsStore(storePath);
|
const store = createFsStore(storePath);
|
||||||
const stringHash = await getStringHash(store);
|
const stringHash = await getStringHash(store);
|
||||||
|
|
||||||
@@ -291,7 +293,9 @@ describe("template get", () => {
|
|||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stderr).toBe("");
|
expect(stderr).toBe("");
|
||||||
expect(stdout).toBe(content);
|
const envelope = JSON.parse(stdout);
|
||||||
|
expect(envelope).toHaveProperty("type");
|
||||||
|
expect(envelope.value).toBe(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("error when template not found", async () => {
|
test("error when template not found", async () => {
|
||||||
@@ -309,27 +313,26 @@ describe("template get", () => {
|
|||||||
const store = createFsStore(storePath);
|
const store = createFsStore(storePath);
|
||||||
const stringHash = await getStringHash(store);
|
const stringHash = await getStringHash(store);
|
||||||
|
|
||||||
// Note: runCli helper trims stdout, so we test with content that doesn't have leading/trailing whitespace
|
// The envelope's value preserves exact whitespace (JSON-escaped),
|
||||||
// The actual CLI preserves whitespace correctly
|
// so trimming the surrounding JSON output is harmless.
|
||||||
const content = "spaces\n\ttabs\t\nmixed";
|
const content = "spaces\n\ttabs\t\nmixed";
|
||||||
await runCli("template", "set", stringHash, "--inline", content);
|
await runCli("template", "set", stringHash, "--inline", content);
|
||||||
|
|
||||||
const { stdout } = await runCli("template", "get", stringHash);
|
const { stdout } = await runCli("template", "get", stringHash);
|
||||||
|
|
||||||
expect(stdout).toBe(content);
|
expect(JSON.parse(stdout).value).toBe(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("support multi-line templates", async () => {
|
test("support multi-line templates", async () => {
|
||||||
const store = createFsStore(storePath);
|
const store = createFsStore(storePath);
|
||||||
const stringHash = await getStringHash(store);
|
const stringHash = await getStringHash(store);
|
||||||
|
|
||||||
// Note: runCli helper trims stdout, so trailing newline will be removed
|
|
||||||
const multiline = "Line 1\nLine 2\nLine 3";
|
const multiline = "Line 1\nLine 2\nLine 3";
|
||||||
await runCli("template", "set", stringHash, "--inline", multiline);
|
await runCli("template", "set", stringHash, "--inline", multiline);
|
||||||
|
|
||||||
const { stdout } = await runCli("template", "get", stringHash);
|
const { stdout } = await runCli("template", "get", stringHash);
|
||||||
|
|
||||||
expect(stdout).toBe(multiline);
|
expect(JSON.parse(stdout).value).toBe(multiline);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -346,34 +349,40 @@ describe("template list", () => {
|
|||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const output = JSON.parse(stdout);
|
const envelope = JSON.parse(stdout);
|
||||||
expect(Array.isArray(output)).toBe(true);
|
expect(envelope).toHaveProperty("type");
|
||||||
expect(output.length).toBeGreaterThanOrEqual(1);
|
expect(Array.isArray(envelope.value)).toBe(true);
|
||||||
|
expect(envelope.value.length).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
// Check structure
|
// Check structure
|
||||||
const item = output[0];
|
const item = envelope.value[0];
|
||||||
expect(item).toHaveProperty("schemaHash");
|
expect(item).toHaveProperty("schemaHash");
|
||||||
expect(item).toHaveProperty("preview");
|
expect(item).toHaveProperty("contentHash");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("preview truncation for long content", async () => {
|
test("entry contentHash matches set result", async () => {
|
||||||
const store = createFsStore(storePath);
|
const store = createFsStore(storePath);
|
||||||
const stringHash = await getStringHash(store);
|
const stringHash = await getStringHash(store);
|
||||||
|
|
||||||
const longContent = "a".repeat(200);
|
const { stdout: setOut } = await runCli(
|
||||||
await runCli("template", "set", stringHash, "--inline", longContent);
|
"template",
|
||||||
|
"set",
|
||||||
|
stringHash,
|
||||||
|
"--inline",
|
||||||
|
"Some template content",
|
||||||
|
);
|
||||||
|
const { contentHash } = JSON.parse(setOut).value;
|
||||||
|
|
||||||
const { stdout } = await runCli("template", "list");
|
const { stdout } = await runCli("template", "list");
|
||||||
|
|
||||||
const output = JSON.parse(stdout) as Array<{
|
const value = JSON.parse(stdout).value as Array<{
|
||||||
schemaHash: string;
|
schemaHash: string;
|
||||||
preview: string;
|
contentHash: string;
|
||||||
}>;
|
}>;
|
||||||
const item = output.find((i) => i.schemaHash === stringHash);
|
const item = value.find((i) => i.schemaHash === stringHash);
|
||||||
expect(item).toBeDefined();
|
expect(item).toBeDefined();
|
||||||
if (item) {
|
if (item) {
|
||||||
expect(item.preview.length).toBeLessThan(longContent.length);
|
expect(item.contentHash).toBe(contentHash);
|
||||||
expect(item.preview).toContain("...");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -382,9 +391,9 @@ describe("template list", () => {
|
|||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const output = JSON.parse(stdout);
|
const envelope = JSON.parse(stdout);
|
||||||
expect(Array.isArray(output)).toBe(true);
|
expect(Array.isArray(envelope.value)).toBe(true);
|
||||||
expect(output.length).toBe(0);
|
expect(envelope.value.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("exclude non-template variables", async () => {
|
test("exclude non-template variables", async () => {
|
||||||
@@ -400,14 +409,14 @@ describe("template list", () => {
|
|||||||
|
|
||||||
const { stdout } = await runCli("template", "list");
|
const { stdout } = await runCli("template", "list");
|
||||||
|
|
||||||
const output = JSON.parse(stdout);
|
const envelope = JSON.parse(stdout);
|
||||||
// Should only contain template variables
|
// Should only contain template variables
|
||||||
for (const item of output) {
|
for (const item of envelope.value) {
|
||||||
expect(item.schemaHash).toBeDefined();
|
expect(item.schemaHash).toBeDefined();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("output JSON array format", async () => {
|
test("output JSON envelope with array value", async () => {
|
||||||
const store = createFsStore(storePath);
|
const store = createFsStore(storePath);
|
||||||
const stringHash = await getStringHash(store);
|
const stringHash = await getStringHash(store);
|
||||||
|
|
||||||
@@ -418,27 +427,8 @@ describe("template list", () => {
|
|||||||
// Should be valid JSON
|
// Should be valid JSON
|
||||||
expect(() => JSON.parse(stdout)).not.toThrow();
|
expect(() => JSON.parse(stdout)).not.toThrow();
|
||||||
|
|
||||||
const output = JSON.parse(stdout);
|
const envelope = JSON.parse(stdout);
|
||||||
expect(Array.isArray(output)).toBe(true);
|
expect(Array.isArray(envelope.value)).toBe(true);
|
||||||
});
|
|
||||||
|
|
||||||
test("preview shows beginning of content", async () => {
|
|
||||||
const store = createFsStore(storePath);
|
|
||||||
const stringHash = await getStringHash(store);
|
|
||||||
|
|
||||||
const content = "Start of template...";
|
|
||||||
await runCli("template", "set", stringHash, "--inline", content);
|
|
||||||
|
|
||||||
const { stdout } = await runCli("template", "list");
|
|
||||||
|
|
||||||
const output = JSON.parse(stdout) as Array<{
|
|
||||||
schemaHash: string;
|
|
||||||
preview: string;
|
|
||||||
}>;
|
|
||||||
const item = output.find((i) => i.schemaHash === stringHash);
|
|
||||||
if (item) {
|
|
||||||
expect(item.preview).toContain("Start");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -458,9 +448,10 @@ describe("template delete", () => {
|
|||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(stderr).toBe("");
|
expect(stderr).toBe("");
|
||||||
|
|
||||||
const output = JSON.parse(stdout);
|
const envelope = JSON.parse(stdout);
|
||||||
expect(output).toHaveProperty("deleted");
|
expect(envelope).toHaveProperty("type");
|
||||||
expect(output.deleted).toBe(true);
|
expect(envelope.value).toHaveProperty("deleted");
|
||||||
|
expect(envelope.value.deleted).toBe(true);
|
||||||
|
|
||||||
// Verify template is gone
|
// Verify template is gone
|
||||||
const { exitCode: getExitCode } = await runCli(
|
const { exitCode: getExitCode } = await runCli(
|
||||||
@@ -495,13 +486,13 @@ describe("template delete", () => {
|
|||||||
|
|
||||||
// Verify second still exists
|
// Verify second still exists
|
||||||
const { stdout } = await runCli("template", "list");
|
const { stdout } = await runCli("template", "list");
|
||||||
const output = JSON.parse(stdout) as Array<{
|
const value = JSON.parse(stdout).value as Array<{
|
||||||
schemaHash: string;
|
schemaHash: string;
|
||||||
preview: string;
|
contentHash: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
// Should not find deleted template
|
// Should not find deleted template
|
||||||
const deleted = output.find((i) => i.schemaHash === stringHash);
|
const deleted = value.find((i) => i.schemaHash === stringHash);
|
||||||
expect(deleted).toBeUndefined();
|
expect(deleted).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -519,7 +510,7 @@ describe("template delete", () => {
|
|||||||
"--inline",
|
"--inline",
|
||||||
"Content",
|
"Content",
|
||||||
);
|
);
|
||||||
const { contentHash } = JSON.parse(setOut);
|
const { contentHash } = JSON.parse(setOut).value;
|
||||||
|
|
||||||
// Delete the template variable
|
// Delete the template variable
|
||||||
await runCli("template", "delete", stringHash);
|
await runCli("template", "delete", stringHash);
|
||||||
@@ -577,7 +568,7 @@ describe("template integration", () => {
|
|||||||
stringHash,
|
stringHash,
|
||||||
);
|
);
|
||||||
expect(getExit).toBe(0);
|
expect(getExit).toBe(0);
|
||||||
expect(getOut).toBe(content);
|
expect(JSON.parse(getOut).value).toBe(content);
|
||||||
|
|
||||||
// List
|
// List
|
||||||
const { stdout: listOut, exitCode: listExit } = await runCli(
|
const { stdout: listOut, exitCode: listExit } = await runCli(
|
||||||
@@ -585,7 +576,7 @@ describe("template integration", () => {
|
|||||||
"list",
|
"list",
|
||||||
);
|
);
|
||||||
expect(listExit).toBe(0);
|
expect(listExit).toBe(0);
|
||||||
const listData = JSON.parse(listOut);
|
const listData = JSON.parse(listOut).value;
|
||||||
expect(listData.length).toBeGreaterThan(0);
|
expect(listData.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Delete
|
// Delete
|
||||||
@@ -626,8 +617,8 @@ describe("template integration", () => {
|
|||||||
|
|
||||||
// List should show all
|
// List should show all
|
||||||
const { stdout } = await runCli("template", "list");
|
const { stdout } = await runCli("template", "list");
|
||||||
const output = JSON.parse(stdout);
|
const value = JSON.parse(stdout).value;
|
||||||
expect(output.length).toBeGreaterThanOrEqual(1);
|
expect(value.length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user