Files
openclaw/src/oc-path/tests/universal.test.ts
Gio Della-Libera bc735f4fde feat(workspace): oc-path addressing substrate + openclaw path CLI (md/jsonc/jsonl/yaml) (#78678)
Implements #78051 — oc:// addressing substrate for workspace files.

New src/oc-path/ substrate (parser/formatter, per-kind parse+emit for
md/jsonc/jsonl/yaml, universal resolveOcPath/setOcPath/findOcPaths verbs,
sentinel emit guard) + openclaw path resolve|find|set|validate|emit CLI +
docs/cli/path.md reference page + CHANGELOG entry.

Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
2026-05-07 22:26:28 -07:00

476 lines
17 KiB
TypeScript

/**
* Universal verbs — `setOcPath` + `resolveOcPath` test surface.
*
* Every test exercises the universal entry point. The substrate
* dispatches via `ast.kind` and coerces value strings based on AST
* shape at the path location.
*/
import { describe, expect, it } from 'vitest';
import { emitJsonc } from '../jsonc/emit.js';
import { parseJsonc } from '../jsonc/parse.js';
import { emitJsonl } from '../jsonl/emit.js';
import { parseJsonl } from '../jsonl/parse.js';
import { emitMd } from '../emit.js';
import { parseMd } from '../parse.js';
import { parseOcPath } from '../oc-path.js';
import {
detectInsertion,
resolveOcPath,
setOcPath,
} from '../universal.js';
// ---------- detectInsertion ------------------------------------------------
describe('detectInsertion', () => {
it('returns null for plain paths', () => {
expect(detectInsertion(parseOcPath('oc://X.md/section/item/field'))).toBeNull();
});
it('detects bare `+` end-insertion at section', () => {
const info = detectInsertion(parseOcPath('oc://X.md/tools/+'));
expect(info?.marker).toBe('+');
expect(info?.parentPath.section).toBe('tools');
expect(info?.parentPath.item).toBeUndefined();
});
it('detects `+key` keyed insertion', () => {
const info = detectInsertion(parseOcPath('oc://config/plugins/+gitlab'));
expect(info?.marker).toEqual({ kind: 'keyed', key: 'gitlab' });
});
it('detects `+nnn` indexed insertion', () => {
const info = detectInsertion(parseOcPath('oc://config/items/+2'));
expect(info?.marker).toEqual({ kind: 'indexed', index: 2 });
});
it('detects file-root insertion', () => {
const info = detectInsertion(parseOcPath('oc://session.jsonl/+'));
expect(info?.marker).toBe('+');
expect(info?.parentPath.section).toBeUndefined();
});
});
// ---------- resolveOcPath — universal across kinds -------------------------
describe('resolveOcPath — md AST', () => {
const md = parseMd(
'---\nname: github\n---\n\n## Boundaries\n\n- enabled: true\n',
).ast;
it('returns leaf with valueText for frontmatter entry', () => {
const m = resolveOcPath(md, parseOcPath('oc://X.md/[frontmatter]/name'));
expect(m).toMatchObject({ kind: 'leaf', valueText: 'github', leafType: 'string' });
});
it('returns leaf for item-field', () => {
const m = resolveOcPath(
md,
parseOcPath('oc://X.md/boundaries/enabled/enabled'),
);
expect(m).toMatchObject({ kind: 'leaf', valueText: 'true', leafType: 'string' });
});
it('returns node for block', () => {
const m = resolveOcPath(md, parseOcPath('oc://X.md/boundaries'));
expect(m).toMatchObject({ kind: 'node', descriptor: 'md-block' });
});
it('returns root for file-only path', () => {
const m = resolveOcPath(md, parseOcPath('oc://X.md'));
expect(m?.kind).toBe('root');
});
it('returns null for unresolved', () => {
expect(resolveOcPath(md, parseOcPath('oc://X.md/missing'))).toBeNull();
});
});
describe('resolveOcPath — jsonc AST', () => {
const ast = parseJsonc('{ "k": 42, "s": "x", "b": true, "n": null, "arr": [1,2,3] }').ast;
it('returns leaf:number for numeric value', () => {
const m = resolveOcPath(ast, parseOcPath('oc://config/k'));
expect(m).toMatchObject({ kind: 'leaf', valueText: '42', leafType: 'number' });
});
it('returns leaf:string for string value', () => {
const m = resolveOcPath(ast, parseOcPath('oc://config/s'));
expect(m).toMatchObject({ kind: 'leaf', valueText: 'x', leafType: 'string' });
});
it('returns leaf:boolean for bool value', () => {
const m = resolveOcPath(ast, parseOcPath('oc://config/b'));
expect(m).toMatchObject({ kind: 'leaf', valueText: 'true', leafType: 'boolean' });
});
it('returns leaf:null for null value', () => {
const m = resolveOcPath(ast, parseOcPath('oc://config/n'));
expect(m).toMatchObject({ kind: 'leaf', valueText: 'null', leafType: 'null' });
});
it('returns node:jsonc-array for array value', () => {
const m = resolveOcPath(ast, parseOcPath('oc://config/arr'));
expect(m).toMatchObject({ kind: 'node', descriptor: 'jsonc-array' });
});
it('returns leaf at array index', () => {
const m = resolveOcPath(ast, parseOcPath('oc://config/arr.1'));
expect(m).toMatchObject({ kind: 'leaf', valueText: '2', leafType: 'number' });
});
});
describe('resolveOcPath — jsonl AST', () => {
const ast = parseJsonl('{"event":"start","n":1}\n{"event":"step","n":2}\n').ast;
it('returns node:jsonl-line for line address', () => {
const m = resolveOcPath(ast, parseOcPath('oc://log/L1'));
expect(m).toMatchObject({ kind: 'node', descriptor: 'jsonl-line' });
});
it('returns leaf for field on line', () => {
const m = resolveOcPath(ast, parseOcPath('oc://log/L2/event'));
expect(m).toMatchObject({ kind: 'leaf', valueText: 'step', leafType: 'string' });
});
it('returns leaf:number for $last/n', () => {
const m = resolveOcPath(ast, parseOcPath('oc://log/$last/n'));
expect(m).toMatchObject({ kind: 'leaf', valueText: '2', leafType: 'number' });
});
});
describe('resolveOcPath — insertion-point detection', () => {
it('returns insertion-point for md section append', () => {
const md = parseMd('## Tools\n').ast;
const m = resolveOcPath(md, parseOcPath('oc://X.md/tools/+'));
expect(m).toMatchObject({ kind: 'insertion-point', container: 'md-section' });
});
it('returns insertion-point for md file-level', () => {
const md = parseMd('## Tools\n').ast;
const m = resolveOcPath(md, parseOcPath('oc://X.md/+'));
expect(m).toMatchObject({ kind: 'insertion-point', container: 'md-file' });
});
it('returns insertion-point for md frontmatter +key', () => {
const md = parseMd('---\nname: x\n---\n').ast;
const m = resolveOcPath(
md,
parseOcPath('oc://X.md/[frontmatter]/+description'),
);
expect(m).toMatchObject({ kind: 'insertion-point', container: 'md-frontmatter' });
});
it('returns insertion-point for jsonc array +', () => {
const ast = parseJsonc('{ "items": [1,2,3] }').ast;
const m = resolveOcPath(ast, parseOcPath('oc://config/items/+'));
expect(m).toMatchObject({ kind: 'insertion-point', container: 'jsonc-array' });
});
it('returns insertion-point for jsonc object +key', () => {
const ast = parseJsonc('{ "plugins": {} }').ast;
const m = resolveOcPath(ast, parseOcPath('oc://config/plugins/+gitlab'));
expect(m).toMatchObject({ kind: 'insertion-point', container: 'jsonc-object' });
});
it('returns insertion-point for jsonl file-root +', () => {
const ast = parseJsonl('').ast;
const m = resolveOcPath(ast, parseOcPath('oc://log/+'));
expect(m).toMatchObject({ kind: 'insertion-point', container: 'jsonl-file' });
});
it('returns null when insertion target is not a container', () => {
const ast = parseJsonc('{ "k": 42 }').ast;
const m = resolveOcPath(ast, parseOcPath('oc://config/k/+'));
expect(m).toBeNull();
});
});
// ---------- setOcPath — leaf assignment ------------------------------------
describe('setOcPath — md leaf', () => {
it('replaces frontmatter value', () => {
const md = parseMd('---\nname: old\n---\n').ast;
const r = setOcPath(md, parseOcPath('oc://X.md/[frontmatter]/name'), 'new');
expect(r.ok).toBe(true);
if (r.ok) {expect(r.ast.kind === 'md' && r.ast.frontmatter[0]?.value).toBe('new');}
});
it('replaces item kv value', () => {
const md = parseMd('## Boundaries\n\n- timeout: 5\n').ast;
const r = setOcPath(md, parseOcPath('oc://X.md/boundaries/timeout/timeout'), '60');
expect(r.ok).toBe(true);
if (r.ok) {
const out = emitMd(r.ast as Parameters<typeof emitMd>[0]);
expect(out).toContain('- timeout: 60');
}
});
it('returns unresolved for missing path', () => {
const md = parseMd('').ast;
const r = setOcPath(md, parseOcPath('oc://X.md/missing/x/x'), 'v');
expect(r.ok).toBe(false);
if (!r.ok) {expect(r.reason).toBe('unresolved');}
});
});
describe('setOcPath — jsonc leaf with coercion', () => {
it('replaces string leaf with string value', () => {
const ast = parseJsonc('{ "k": "old" }').ast;
const r = setOcPath(ast, parseOcPath('oc://config/k'), 'new');
expect(r.ok).toBe(true);
if (r.ok) {
const ast2 = r.ast as Parameters<typeof emitJsonc>[0];
expect(JSON.parse(emitJsonc(ast2))).toEqual({ k: 'new' });
}
});
it('coerces value to number when leaf was number', () => {
const ast = parseJsonc('{ "k": 1 }').ast;
const r = setOcPath(ast, parseOcPath('oc://config/k'), '42');
expect(r.ok).toBe(true);
if (r.ok) {
const ast2 = r.ast as Parameters<typeof emitJsonc>[0];
expect(JSON.parse(emitJsonc(ast2))).toEqual({ k: 42 });
}
});
it('coerces "true"/"false" when leaf was boolean', () => {
const ast = parseJsonc('{ "k": true }').ast;
const r = setOcPath(ast, parseOcPath('oc://config/k'), 'false');
expect(r.ok).toBe(true);
if (r.ok) {
const ast2 = r.ast as Parameters<typeof emitJsonc>[0];
expect(JSON.parse(emitJsonc(ast2))).toEqual({ k: false });
}
});
it('rejects non-numeric string for number leaf', () => {
const ast = parseJsonc('{ "k": 1 }').ast;
const r = setOcPath(ast, parseOcPath('oc://config/k'), 'not-a-number');
expect(r.ok).toBe(false);
if (!r.ok) {expect(r.reason).toBe('parse-error');}
});
it('rejects non-bool string for boolean leaf', () => {
const ast = parseJsonc('{ "k": true }').ast;
const r = setOcPath(ast, parseOcPath('oc://config/k'), 'maybe');
expect(r.ok).toBe(false);
if (!r.ok) {expect(r.reason).toBe('parse-error');}
});
});
describe('setOcPath — jsonl leaf', () => {
it('replaces field on a value line with coercion', () => {
const ast = parseJsonl('{"event":"start","n":1}\n').ast;
const r = setOcPath(ast, parseOcPath('oc://log/L1/n'), '42');
expect(r.ok).toBe(true);
if (r.ok) {
const out = emitJsonl(r.ast as Parameters<typeof emitJsonl>[0]);
expect(JSON.parse(out.split('\n')[0])).toEqual({ event: 'start', n: 42 });
}
});
it('replaces whole line via JSON value', () => {
const ast = parseJsonl('{"event":"start"}\n').ast;
const r = setOcPath(ast, parseOcPath('oc://log/L1'), '{"event":"replaced"}');
expect(r.ok).toBe(true);
if (r.ok) {
const out = emitJsonl(r.ast as Parameters<typeof emitJsonl>[0]);
expect(JSON.parse(out.split('\n')[0])).toEqual({ event: 'replaced' });
}
});
it('rejects malformed JSON for whole-line replacement', () => {
const ast = parseJsonl('{"event":"start"}\n').ast;
const r = setOcPath(ast, parseOcPath('oc://log/L1'), 'not json');
expect(r.ok).toBe(false);
if (!r.ok) {expect(r.reason).toBe('parse-error');}
});
});
// ---------- setOcPath — insertion ------------------------------------------
describe('setOcPath — md insertion', () => {
it('appends item to section with `+`', () => {
const md = parseMd('## Tools\n\n- gh: GitHub CLI\n').ast;
const r = setOcPath(md, parseOcPath('oc://X.md/tools/+'), 'docker: container CLI');
expect(r.ok).toBe(true);
if (r.ok) {
const out = emitMd(r.ast as Parameters<typeof emitMd>[0]);
expect(out).toContain('- gh: GitHub CLI');
expect(out).toContain('- docker: container CLI');
}
});
it('appends new section at file root with `+`', () => {
const md = parseMd('## Existing\n').ast;
const r = setOcPath(md, parseOcPath('oc://X.md/+'), 'New Section');
expect(r.ok).toBe(true);
if (r.ok) {
const out = emitMd(r.ast as Parameters<typeof emitMd>[0]);
expect(out).toContain('## Existing');
expect(out).toContain('## New Section');
}
});
it('adds new frontmatter key with +key', () => {
const md = parseMd('---\nname: x\n---\n').ast;
const r = setOcPath(
md,
parseOcPath('oc://X.md/[frontmatter]/+description'),
'a new description',
);
expect(r.ok).toBe(true);
if (r.ok) {
const out = emitMd(r.ast as Parameters<typeof emitMd>[0]);
expect(out).toContain('description: a new description');
}
});
it('rejects duplicate frontmatter key on insertion', () => {
const md = parseMd('---\nname: x\n---\n').ast;
const r = setOcPath(md, parseOcPath('oc://X.md/[frontmatter]/+name'), 'y');
expect(r.ok).toBe(false);
if (!r.ok) {expect(r.reason).toBe('type-mismatch');}
});
});
describe('setOcPath — jsonc insertion', () => {
it('appends to array with `+`', () => {
const ast = parseJsonc('{ "items": [1, 2] }').ast;
const r = setOcPath(ast, parseOcPath('oc://config/items/+'), '3');
expect(r.ok).toBe(true);
if (r.ok) {
const ast2 = r.ast as Parameters<typeof emitJsonc>[0];
expect(JSON.parse(emitJsonc(ast2))).toEqual({ items: [1, 2, 3] });
}
});
it('inserts at index with `+nnn`', () => {
const ast = parseJsonc('{ "items": [1, 3] }').ast;
const r = setOcPath(ast, parseOcPath('oc://config/items/+1'), '2');
expect(r.ok).toBe(true);
if (r.ok) {
const ast2 = r.ast as Parameters<typeof emitJsonc>[0];
expect(JSON.parse(emitJsonc(ast2))).toEqual({ items: [1, 2, 3] });
}
});
it('adds object key with `+key`', () => {
const ast = parseJsonc('{ "plugins": { "github": "tok" } }').ast;
const r = setOcPath(
ast,
parseOcPath('oc://config/plugins/+gitlab'),
'"new-tok"',
);
expect(r.ok).toBe(true);
if (r.ok) {
const ast2 = r.ast as Parameters<typeof emitJsonc>[0];
expect(JSON.parse(emitJsonc(ast2))).toEqual({
plugins: { github: 'tok', gitlab: 'new-tok' },
});
}
});
it('rejects duplicate object key', () => {
const ast = parseJsonc('{ "plugins": { "github": "x" } }').ast;
const r = setOcPath(ast, parseOcPath('oc://config/plugins/+github'), '"y"');
expect(r.ok).toBe(false);
if (!r.ok) {expect(r.reason).toBe('unresolved');}
});
it('rejects +key on array', () => {
const ast = parseJsonc('{ "items": [1, 2] }').ast;
const r = setOcPath(ast, parseOcPath('oc://config/items/+abc'), '3');
expect(r.ok).toBe(false);
if (!r.ok) {expect(r.reason).toBe('type-mismatch');}
});
it('inserts complex object via JSON value', () => {
const ast = parseJsonc('{ "plugins": {} }').ast;
const r = setOcPath(
ast,
parseOcPath('oc://config/plugins/+gitlab'),
'{"token":"xyz","enabled":true}',
);
expect(r.ok).toBe(true);
if (r.ok) {
const ast2 = r.ast as Parameters<typeof emitJsonc>[0];
expect(JSON.parse(emitJsonc(ast2))).toEqual({
plugins: { gitlab: { token: 'xyz', enabled: true } },
});
}
});
});
describe('setOcPath — jsonl insertion (session append)', () => {
it('appends a JSON line with `+`', () => {
const ast = parseJsonl('{"event":"start"}\n').ast;
const r = setOcPath(
ast,
parseOcPath('oc://log/+'),
'{"event":"step","n":1}',
);
expect(r.ok).toBe(true);
if (r.ok) {
const out = emitJsonl(r.ast as Parameters<typeof emitJsonl>[0]);
const lines = out.split('\n').filter((l) => l.length > 0);
expect(lines).toHaveLength(2);
expect(JSON.parse(lines[1])).toEqual({ event: 'step', n: 1 });
}
});
it('rejects malformed JSON value', () => {
const ast = parseJsonl('').ast;
const r = setOcPath(ast, parseOcPath('oc://log/+'), 'not json');
expect(r.ok).toBe(false);
if (!r.ok) {expect(r.reason).toBe('parse-error');}
});
it('rejects non-root insertion target', () => {
const ast = parseJsonl('{"a":1}\n').ast;
const r = setOcPath(ast, parseOcPath('oc://log/L1/+'), '{}');
expect(r.ok).toBe(false);
});
});
// ---------- Cross-cutting properties ---------------------------------------
describe('setOcPath — cross-cutting properties', () => {
it('is non-mutating across all kinds', () => {
const md = parseMd('---\nname: x\n---\n').ast;
const before = JSON.stringify(md);
setOcPath(md, parseOcPath('oc://X.md/[frontmatter]/name'), 'new');
expect(JSON.stringify(md)).toBe(before);
const jsonc = parseJsonc('{ "k": 1 }').ast;
const before2 = JSON.stringify(jsonc);
setOcPath(jsonc, parseOcPath('oc://config/k'), '99');
expect(JSON.stringify(jsonc)).toBe(before2);
const jsonl = parseJsonl('{"a":1}\n').ast;
const before3 = JSON.stringify(jsonl);
setOcPath(jsonl, parseOcPath('oc://log/L1/a'), '99');
expect(JSON.stringify(jsonl)).toBe(before3);
});
it('returns ok-tagged result with new ast on success', () => {
const md = parseMd('---\nname: x\n---\n').ast;
const r = setOcPath(md, parseOcPath('oc://X.md/[frontmatter]/name'), 'y');
expect(r.ok).toBe(true);
if (r.ok) {
expect(r.ast.kind).toBe('md');
}
});
it('returns failure-tagged result with reason on unresolved', () => {
const ast = parseJsonc('{}').ast;
const r = setOcPath(ast, parseOcPath('oc://config/missing'), 'v');
expect(r.ok).toBe(false);
if (!r.ok) {
expect(r.reason).toBeDefined();
expect(typeof r.reason).toBe('string');
}
});
});