mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 20:30:44 +00:00
test: require oc-path resolver matches
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { parseJsonl } from '../../jsonl/parse.js';
|
||||
import { resolveJsonlOcPath } from '../../jsonl/resolve.js';
|
||||
import { parseOcPath } from '../../oc-path.js';
|
||||
import { resolveOcPath } from '../../universal.js';
|
||||
import { findOcPaths } from '../../find.js';
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { findOcPaths } from "../../find.js";
|
||||
import { parseJsonl } from "../../jsonl/parse.js";
|
||||
import { resolveJsonlOcPath } from "../../jsonl/resolve.js";
|
||||
import { parseOcPath } from "../../oc-path.js";
|
||||
import { resolveOcPath } from "../../universal.js";
|
||||
|
||||
const log = `{"event":"start","ts":1}
|
||||
{"event":"step","n":1,"result":{"ok":true,"detail":"a"}}
|
||||
@@ -16,51 +16,51 @@ function rs(ocPath: string) {
|
||||
return resolveJsonlOcPath(ast, parseOcPath(ocPath));
|
||||
}
|
||||
|
||||
describe('resolveJsonlOcPath', () => {
|
||||
it('returns root when no segments are given', () => {
|
||||
expect(rs('oc://session-events')?.kind).toBe('root');
|
||||
describe("resolveJsonlOcPath", () => {
|
||||
it("returns root when no segments are given", () => {
|
||||
expect(rs("oc://session-events")?.kind).toBe("root");
|
||||
});
|
||||
|
||||
it('addresses an entire line by line number', () => {
|
||||
const m = rs('oc://session-events/L1');
|
||||
expect(m?.kind).toBe('line');
|
||||
it("addresses an entire line by line number", () => {
|
||||
const m = rs("oc://session-events/L1");
|
||||
expect(m?.kind).toBe("line");
|
||||
});
|
||||
|
||||
it('addresses fields under a line via item segment', () => {
|
||||
const m = rs('oc://session-events/L2/event');
|
||||
expect(m?.kind).toBe('object-entry');
|
||||
if (m?.kind === 'object-entry') {
|
||||
expect(m.node.value).toMatchObject({ kind: 'string', value: 'step' });
|
||||
it("addresses fields under a line via item segment", () => {
|
||||
const m = rs("oc://session-events/L2/event");
|
||||
expect(m?.kind).toBe("object-entry");
|
||||
if (m?.kind === "object-entry") {
|
||||
expect(m.node.value).toMatchObject({ kind: "string", value: "step" });
|
||||
}
|
||||
});
|
||||
|
||||
it('descends via dotted item paths', () => {
|
||||
const m = rs('oc://session-events/L2/result.ok');
|
||||
expect(m?.kind).toBe('object-entry');
|
||||
if (m?.kind === 'object-entry') {
|
||||
expect(m.node.value).toMatchObject({ kind: 'boolean', value: true });
|
||||
it("descends via dotted item paths", () => {
|
||||
const m = rs("oc://session-events/L2/result.ok");
|
||||
expect(m?.kind).toBe("object-entry");
|
||||
if (m?.kind === "object-entry") {
|
||||
expect(m.node.value).toMatchObject({ kind: "boolean", value: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('resolves $last to the most recent value line', () => {
|
||||
const m = rs('oc://session-events/$last/event');
|
||||
expect(m?.kind).toBe('object-entry');
|
||||
if (m?.kind === 'object-entry') {
|
||||
expect(m.node.value).toMatchObject({ kind: 'string', value: 'end' });
|
||||
it("resolves $last to the most recent value line", () => {
|
||||
const m = rs("oc://session-events/$last/event");
|
||||
expect(m?.kind).toBe("object-entry");
|
||||
if (m?.kind === "object-entry") {
|
||||
expect(m.node.value).toMatchObject({ kind: "string", value: "end" });
|
||||
}
|
||||
});
|
||||
|
||||
it('returns null for unknown line addresses', () => {
|
||||
expect(rs('oc://session-events/L99')).toBeNull();
|
||||
expect(rs('oc://session-events/garbage')).toBeNull();
|
||||
it("returns null for unknown line addresses", () => {
|
||||
expect(rs("oc://session-events/L99")).toBeNull();
|
||||
expect(rs("oc://session-events/garbage")).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when descending into a blank line', () => {
|
||||
expect(rs('oc://session-events/L3/anything')).toBeNull();
|
||||
it("returns null when descending into a blank line", () => {
|
||||
expect(rs("oc://session-events/L3/anything")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveJsonlToUniversal — file-relative line metadata (regression)', () => {
|
||||
describe("resolveJsonlToUniversal — file-relative line metadata (regression)", () => {
|
||||
// Regression: surfaced via the openclaw-path CLI scenario run on
|
||||
// a multi-line session.jsonl. Every match returned `line: 1`
|
||||
// because the inside-line jsonc parser numbers from 1 within each
|
||||
@@ -68,30 +68,28 @@ describe('resolveJsonlToUniversal — file-relative line metadata (regression)',
|
||||
// number over the JsonlLine's file-relative line.
|
||||
|
||||
const log = [
|
||||
'{"event":"start"}', // line 1
|
||||
'{"event":"step","n":1}', // line 2
|
||||
'{"event":"step","n":2}', // line 3
|
||||
'{"event":"end"}', // line 4
|
||||
'', // line 5 (blank)
|
||||
].join('\n');
|
||||
'{"event":"start"}', // line 1
|
||||
'{"event":"step","n":1}', // line 2
|
||||
'{"event":"step","n":2}', // line 3
|
||||
'{"event":"end"}', // line 4
|
||||
"", // line 5 (blank)
|
||||
].join("\n");
|
||||
|
||||
it('resolves L2/event with line=2 (not 1)', () => {
|
||||
it("resolves L2/event with line=2 (not 1)", () => {
|
||||
const { ast } = parseJsonl(log);
|
||||
const m = resolveOcPath(ast, parseOcPath('oc://session.jsonl/L2/event'));
|
||||
expect(m).not.toBeNull();
|
||||
if (m !== null) {expect(m.line).toBe(2);}
|
||||
const m = resolveOcPath(ast, parseOcPath("oc://session.jsonl/L2/event"));
|
||||
expect(m).toEqual(expect.objectContaining({ line: 2 }));
|
||||
});
|
||||
|
||||
it('resolves L4/event with line=4', () => {
|
||||
it("resolves L4/event with line=4", () => {
|
||||
const { ast } = parseJsonl(log);
|
||||
const m = resolveOcPath(ast, parseOcPath('oc://session.jsonl/L4/event'));
|
||||
expect(m).not.toBeNull();
|
||||
if (m !== null) {expect(m.line).toBe(4);}
|
||||
const m = resolveOcPath(ast, parseOcPath("oc://session.jsonl/L4/event"));
|
||||
expect(m).toEqual(expect.objectContaining({ line: 4 }));
|
||||
});
|
||||
|
||||
it('findOcPaths over wildcard surfaces correct file-relative lines', () => {
|
||||
it("findOcPaths over wildcard surfaces correct file-relative lines", () => {
|
||||
const { ast } = parseJsonl(log);
|
||||
const matches = findOcPaths(ast, parseOcPath('oc://session.jsonl/*/event'));
|
||||
const matches = findOcPaths(ast, parseOcPath("oc://session.jsonl/*/event"));
|
||||
expect(matches).toHaveLength(4);
|
||||
const lines = matches.map((m) => m.match.line);
|
||||
expect(lines).toEqual([1, 2, 3, 4]);
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
* across re-parses. OcPath round-trip via the AST (slugs in OcPath
|
||||
* must round-trip back to the resolved node).
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { emitMd } from '../../emit.js';
|
||||
import { formatOcPath, parseOcPath } from '../../oc-path.js';
|
||||
import { parseMd } from '../../parse.js';
|
||||
import { resolveMdOcPath as resolveOcPath } from '../../resolve.js';
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { emitMd } from "../../emit.js";
|
||||
import { formatOcPath, parseOcPath } from "../../oc-path.js";
|
||||
import { parseMd } from "../../parse.js";
|
||||
import { resolveMdOcPath as resolveOcPath } from "../../resolve.js";
|
||||
|
||||
const SAMPLE = `---
|
||||
name: github
|
||||
@@ -29,60 +29,60 @@ Preamble.
|
||||
- curl: HTTP client
|
||||
`;
|
||||
|
||||
describe('wave-13 cross-cutting', () => {
|
||||
it('CC-01 parse → resolve → emit pipeline (block)', () => {
|
||||
describe("wave-13 cross-cutting", () => {
|
||||
it("CC-01 parse → resolve → emit pipeline (block)", () => {
|
||||
const { ast } = parseMd(SAMPLE);
|
||||
const m = resolveOcPath(ast, { file: 'AGENTS.md', section: 'boundaries' });
|
||||
expect(m?.kind).toBe('block');
|
||||
const m = resolveOcPath(ast, { file: "AGENTS.md", section: "boundaries" });
|
||||
expect(m?.kind).toBe("block");
|
||||
expect(emitMd(ast)).toBe(SAMPLE);
|
||||
});
|
||||
|
||||
it('CC-02 OcPath round-trip via AST: parse + resolve + format', () => {
|
||||
it("CC-02 OcPath round-trip via AST: parse + resolve + format", () => {
|
||||
const { ast } = parseMd(SAMPLE);
|
||||
for (const block of ast.blocks) {
|
||||
const path = parseOcPath(`oc://AGENTS.md/${block.slug}`);
|
||||
const m = resolveOcPath(ast, path);
|
||||
expect(m?.kind, `block ${block.slug} should resolve`).toBe('block');
|
||||
expect(m?.kind, `block ${block.slug} should resolve`).toBe("block");
|
||||
// Format the same path back; slug → URI shape should be stable.
|
||||
expect(formatOcPath(path)).toBe(`oc://AGENTS.md/${block.slug}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('CC-03 every item in every block is OcPath-addressable', () => {
|
||||
it("CC-03 every item in every block is OcPath-addressable", () => {
|
||||
const { ast } = parseMd(SAMPLE);
|
||||
for (const block of ast.blocks) {
|
||||
for (const item of block.items) {
|
||||
const path = parseOcPath(`oc://AGENTS.md/${block.slug}/${item.slug}`);
|
||||
const m = resolveOcPath(ast, path);
|
||||
expect(m?.kind, `${block.slug}/${item.slug} should resolve`).toBe('item');
|
||||
expect(m?.kind, `${block.slug}/${item.slug} should resolve`).toBe("item");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('CC-04 every kv item field is OcPath-addressable', () => {
|
||||
it("CC-04 every kv item field is OcPath-addressable", () => {
|
||||
const { ast } = parseMd(SAMPLE);
|
||||
for (const block of ast.blocks) {
|
||||
for (const item of block.items) {
|
||||
if (!item.kv) {continue;}
|
||||
const path = parseOcPath(
|
||||
`oc://AGENTS.md/${block.slug}/${item.slug}/${item.kv.key}`,
|
||||
);
|
||||
if (!item.kv) {
|
||||
continue;
|
||||
}
|
||||
const path = parseOcPath(`oc://AGENTS.md/${block.slug}/${item.slug}/${item.kv.key}`);
|
||||
const m = resolveOcPath(ast, path);
|
||||
expect(m?.kind).toBe('item-field');
|
||||
expect(m?.kind).toBe("item-field");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('CC-05 every frontmatter entry is OcPath-addressable', () => {
|
||||
it("CC-05 every frontmatter entry is OcPath-addressable", () => {
|
||||
const { ast } = parseMd(SAMPLE);
|
||||
for (const fm of ast.frontmatter) {
|
||||
const path = parseOcPath(`oc://AGENTS.md/[frontmatter]/${fm.key}`);
|
||||
const m = resolveOcPath(ast, path);
|
||||
expect(m?.kind).toBe('frontmatter');
|
||||
expect(m?.kind).toBe("frontmatter");
|
||||
}
|
||||
});
|
||||
|
||||
it('CC-06 slugs are stable across re-parses (deterministic)', () => {
|
||||
it("CC-06 slugs are stable across re-parses (deterministic)", () => {
|
||||
const a1 = parseMd(SAMPLE).ast;
|
||||
const a2 = parseMd(SAMPLE).ast;
|
||||
expect(a1.blocks.map((b) => b.slug)).toEqual(a2.blocks.map((b) => b.slug));
|
||||
@@ -91,49 +91,49 @@ describe('wave-13 cross-cutting', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('CC-07 modifying raw + re-parse produces consistent AST shape', () => {
|
||||
it("CC-07 modifying raw + re-parse produces consistent AST shape", () => {
|
||||
const a1 = parseMd(SAMPLE).ast;
|
||||
const modified = SAMPLE.replace('GitHub CLI', 'GitHub command-line interface');
|
||||
const modified = SAMPLE.replace("GitHub CLI", "GitHub command-line interface");
|
||||
const a2 = parseMd(modified).ast;
|
||||
// Block + item count + slugs unchanged.
|
||||
expect(a2.blocks.length).toBe(a1.blocks.length);
|
||||
const a1Tools = a1.blocks.find((b) => b.slug === 'tools');
|
||||
const a2Tools = a2.blocks.find((b) => b.slug === 'tools');
|
||||
const a1Tools = a1.blocks.find((b) => b.slug === "tools");
|
||||
const a2Tools = a2.blocks.find((b) => b.slug === "tools");
|
||||
expect(a2Tools?.items.length).toBe(a1Tools?.items.length);
|
||||
// KV value reflects the change.
|
||||
const ghItem = a2Tools?.items.find((i) => i.kv?.key === 'gh');
|
||||
expect(ghItem?.kv?.value).toBe('GitHub command-line interface');
|
||||
const ghItem = a2Tools?.items.find((i) => i.kv?.key === "gh");
|
||||
expect(ghItem?.kv?.value).toBe("GitHub command-line interface");
|
||||
});
|
||||
|
||||
it('CC-08 unknown OcPath returns null without affecting subsequent valid resolves', () => {
|
||||
it("CC-08 unknown OcPath returns null without affecting subsequent valid resolves", () => {
|
||||
const { ast } = parseMd(SAMPLE);
|
||||
expect(resolveOcPath(ast, { file: 'X.md', section: 'nonexistent' })).toBeNull();
|
||||
expect(resolveOcPath(ast, { file: 'X.md', section: 'tools' })?.kind).toBe('block');
|
||||
expect(resolveOcPath(ast, { file: "X.md", section: "nonexistent" })).toBeNull();
|
||||
expect(resolveOcPath(ast, { file: "X.md", section: "tools" })?.kind).toBe("block");
|
||||
});
|
||||
|
||||
it('CC-09 resolve does not depend on file segment matching', () => {
|
||||
it("CC-09 resolve does not depend on file segment matching", () => {
|
||||
const { ast } = parseMd(SAMPLE);
|
||||
const a = resolveOcPath(ast, { file: 'A.md', section: 'tools' });
|
||||
const b = resolveOcPath(ast, { file: 'B.md', section: 'tools' });
|
||||
const a = resolveOcPath(ast, { file: "A.md", section: "tools" });
|
||||
const b = resolveOcPath(ast, { file: "B.md", section: "tools" });
|
||||
expect(a?.kind).toBe(b?.kind);
|
||||
});
|
||||
|
||||
it('CC-10 round-trip across all 9 valid OcPath shapes', () => {
|
||||
it("CC-10 round-trip across all 9 valid OcPath shapes", () => {
|
||||
const { ast } = parseMd(SAMPLE);
|
||||
const cases = [
|
||||
{ file: 'X.md' },
|
||||
{ file: 'X.md', section: 'tools' },
|
||||
{ file: 'X.md', section: 'tools', item: 'gh' },
|
||||
{ file: 'X.md', section: 'tools', item: 'gh', field: 'gh' },
|
||||
{ file: 'X.md', section: '[frontmatter]', field: 'name' },
|
||||
{ file: 'X.md', section: 'boundaries' },
|
||||
{ file: 'X.md', section: 'boundaries', item: 'never-write-to-etc' },
|
||||
{ file: 'X.md', section: 'boundaries', item: 'always-confirm' },
|
||||
{ file: 'X.md', section: '[frontmatter]', field: 'description' },
|
||||
{ file: "X.md" },
|
||||
{ file: "X.md", section: "tools" },
|
||||
{ file: "X.md", section: "tools", item: "gh" },
|
||||
{ file: "X.md", section: "tools", item: "gh", field: "gh" },
|
||||
{ file: "X.md", section: "[frontmatter]", field: "name" },
|
||||
{ file: "X.md", section: "boundaries" },
|
||||
{ file: "X.md", section: "boundaries", item: "never-write-to-etc" },
|
||||
{ file: "X.md", section: "boundaries", item: "always-confirm" },
|
||||
{ file: "X.md", section: "[frontmatter]", field: "description" },
|
||||
];
|
||||
for (const path of cases) {
|
||||
const m = resolveOcPath(ast, path);
|
||||
expect(m, `failed for ${JSON.stringify(path)}`).not.toBeNull();
|
||||
expect(m, `failed for ${JSON.stringify(path)}`).toEqual(expect.any(Object));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,8 +34,11 @@ function rt(raw: string): string {
|
||||
*/
|
||||
function assertParseable(raw: string): JsoncValue {
|
||||
const result = parseJsonc(raw);
|
||||
expect(result.ast.root).not.toBeNull();
|
||||
return result.ast.root as JsoncValue;
|
||||
expect(result.ast.root).toEqual(expect.any(Object));
|
||||
if (result.ast.root === null) {
|
||||
throw new Error("Expected parseable JSONC root");
|
||||
}
|
||||
return result.ast.root;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
* item returns `null` (not a guess). Frontmatter via the `[frontmatter]`
|
||||
* sentinel section.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { parseMd } from '../../parse.js';
|
||||
import { resolveMdOcPath as resolveOcPath } from '../../resolve.js';
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseMd } from "../../parse.js";
|
||||
import { resolveMdOcPath as resolveOcPath } from "../../resolve.js";
|
||||
|
||||
const SAMPLE = `---
|
||||
name: github
|
||||
@@ -34,37 +34,39 @@ Preamble prose.
|
||||
- item one
|
||||
`;
|
||||
|
||||
describe('wave-08 oc-path-resolver-edges', () => {
|
||||
describe("wave-08 oc-path-resolver-edges", () => {
|
||||
const { ast } = parseMd(SAMPLE);
|
||||
|
||||
it('R-01 root resolves to AST', () => {
|
||||
const m = resolveOcPath(ast, { file: 'X.md' });
|
||||
expect(m?.kind).toBe('root');
|
||||
it("R-01 root resolves to AST", () => {
|
||||
const m = resolveOcPath(ast, { file: "X.md" });
|
||||
expect(m?.kind).toBe("root");
|
||||
});
|
||||
|
||||
it('R-02 block by exact slug', () => {
|
||||
const m = resolveOcPath(ast, { file: 'X.md', section: 'boundaries' });
|
||||
expect(m?.kind).toBe('block');
|
||||
it("R-02 block by exact slug", () => {
|
||||
const m = resolveOcPath(ast, { file: "X.md", section: "boundaries" });
|
||||
expect(m?.kind).toBe("block");
|
||||
});
|
||||
|
||||
it('R-03 block by case-mismatched slug (Boundaries → boundaries)', () => {
|
||||
const m = resolveOcPath(ast, { file: 'X.md', section: 'Boundaries' });
|
||||
expect(m?.kind).toBe('block');
|
||||
it("R-03 block by case-mismatched slug (Boundaries → boundaries)", () => {
|
||||
const m = resolveOcPath(ast, { file: "X.md", section: "Boundaries" });
|
||||
expect(m?.kind).toBe("block");
|
||||
});
|
||||
|
||||
it('R-04 block by uppercased slug', () => {
|
||||
const m = resolveOcPath(ast, { file: 'X.md', section: 'BOUNDARIES' });
|
||||
expect(m?.kind).toBe('block');
|
||||
it("R-04 block by uppercased slug", () => {
|
||||
const m = resolveOcPath(ast, { file: "X.md", section: "BOUNDARIES" });
|
||||
expect(m?.kind).toBe("block");
|
||||
});
|
||||
|
||||
it('R-05 multi-word section by slug', () => {
|
||||
const m = resolveOcPath(ast, { file: 'X.md', section: 'multi-word-section' });
|
||||
expect(m?.kind).toBe('block');
|
||||
if (m?.kind === 'block') {expect(m.node.heading).toBe('Multi-Word Section');}
|
||||
it("R-05 multi-word section by slug", () => {
|
||||
const m = resolveOcPath(ast, { file: "X.md", section: "multi-word-section" });
|
||||
expect(m?.kind).toBe("block");
|
||||
if (m?.kind === "block") {
|
||||
expect(m.node.heading).toBe("Multi-Word Section");
|
||||
}
|
||||
});
|
||||
|
||||
it('R-06 multi-word section by exact heading text (case-folded)', () => {
|
||||
const m = resolveOcPath(ast, { file: 'X.md', section: 'Multi-Word Section' });
|
||||
it("R-06 multi-word section by exact heading text (case-folded)", () => {
|
||||
const m = resolveOcPath(ast, { file: "X.md", section: "Multi-Word Section" });
|
||||
// The OcPath section is matched case-insensitively against block.slug.
|
||||
// Block.slug for "Multi-Word Section" is "multi-word-section", and
|
||||
// path.section.toLowerCase() = "multi-word section" which does NOT
|
||||
@@ -73,163 +75,172 @@ describe('wave-08 oc-path-resolver-edges', () => {
|
||||
expect(m).toBeNull();
|
||||
});
|
||||
|
||||
it('R-07 unknown section returns null', () => {
|
||||
const m = resolveOcPath(ast, { file: 'X.md', section: 'unknown' });
|
||||
it("R-07 unknown section returns null", () => {
|
||||
const m = resolveOcPath(ast, { file: "X.md", section: "unknown" });
|
||||
expect(m).toBeNull();
|
||||
});
|
||||
|
||||
it('R-08 item by slug under known section', () => {
|
||||
it("R-08 item by slug under known section", () => {
|
||||
const m = resolveOcPath(ast, {
|
||||
file: 'X.md',
|
||||
section: 'tools',
|
||||
item: 'gh',
|
||||
file: "X.md",
|
||||
section: "tools",
|
||||
item: "gh",
|
||||
});
|
||||
expect(m?.kind).toBe('item');
|
||||
expect(m?.kind).toBe("item");
|
||||
});
|
||||
|
||||
it('R-09 item slug for KV uses kv.key (gh, not "gh-github-cli")', () => {
|
||||
const m = resolveOcPath(ast, {
|
||||
file: 'X.md',
|
||||
section: 'tools',
|
||||
item: 'gh',
|
||||
file: "X.md",
|
||||
section: "tools",
|
||||
item: "gh",
|
||||
});
|
||||
expect(m).not.toBeNull();
|
||||
if (m?.kind === 'item') {expect(m.node.kv?.value).toBe('GitHub CLI');}
|
||||
expect(m).toEqual(expect.objectContaining({ kind: "item" }));
|
||||
if (m?.kind !== "item") {
|
||||
throw new Error("Expected item match for gh");
|
||||
}
|
||||
expect(m.node.kv?.value).toBe("GitHub CLI");
|
||||
});
|
||||
|
||||
it('R-10 item slug for plain bullet uses text', () => {
|
||||
it("R-10 item slug for plain bullet uses text", () => {
|
||||
const m = resolveOcPath(ast, {
|
||||
file: 'X.md',
|
||||
section: 'boundaries',
|
||||
item: 'never-write-to-etc',
|
||||
file: "X.md",
|
||||
section: "boundaries",
|
||||
item: "never-write-to-etc",
|
||||
});
|
||||
expect(m?.kind).toBe('item');
|
||||
expect(m?.kind).toBe("item");
|
||||
});
|
||||
|
||||
it('R-11 item slug case-insensitive', () => {
|
||||
it("R-11 item slug case-insensitive", () => {
|
||||
const m = resolveOcPath(ast, {
|
||||
file: 'X.md',
|
||||
section: 'tools',
|
||||
item: 'GH',
|
||||
file: "X.md",
|
||||
section: "tools",
|
||||
item: "GH",
|
||||
});
|
||||
expect(m?.kind).toBe('item');
|
||||
expect(m?.kind).toBe("item");
|
||||
});
|
||||
|
||||
it('R-12 item with spaces in key (slugified)', () => {
|
||||
it("R-12 item with spaces in key (slugified)", () => {
|
||||
const m = resolveOcPath(ast, {
|
||||
file: 'X.md',
|
||||
section: 'tools',
|
||||
item: 'the-tool',
|
||||
file: "X.md",
|
||||
section: "tools",
|
||||
item: "the-tool",
|
||||
});
|
||||
expect(m?.kind).toBe('item');
|
||||
if (m?.kind === 'item') {expect(m.node.kv?.value).toBe('with caps and spaces');}
|
||||
expect(m?.kind).toBe("item");
|
||||
if (m?.kind === "item") {
|
||||
expect(m.node.kv?.value).toBe("with caps and spaces");
|
||||
}
|
||||
});
|
||||
|
||||
it('R-13 unknown item returns null', () => {
|
||||
it("R-13 unknown item returns null", () => {
|
||||
const m = resolveOcPath(ast, {
|
||||
file: 'X.md',
|
||||
section: 'tools',
|
||||
item: 'nonexistent',
|
||||
file: "X.md",
|
||||
section: "tools",
|
||||
item: "nonexistent",
|
||||
});
|
||||
expect(m).toBeNull();
|
||||
});
|
||||
|
||||
it('R-14 item-field matches kv.key (case-insensitive)', () => {
|
||||
it("R-14 item-field matches kv.key (case-insensitive)", () => {
|
||||
const m = resolveOcPath(ast, {
|
||||
file: 'X.md',
|
||||
section: 'tools',
|
||||
item: 'gh',
|
||||
field: 'gh',
|
||||
file: "X.md",
|
||||
section: "tools",
|
||||
item: "gh",
|
||||
field: "gh",
|
||||
});
|
||||
expect(m?.kind).toBe('item-field');
|
||||
expect(m?.kind).toBe("item-field");
|
||||
});
|
||||
|
||||
it('R-15 field on plain (non-kv) item returns null', () => {
|
||||
it("R-15 field on plain (non-kv) item returns null", () => {
|
||||
const m = resolveOcPath(ast, {
|
||||
file: 'X.md',
|
||||
section: 'boundaries',
|
||||
item: 'never-write-to-etc',
|
||||
field: 'risk',
|
||||
file: "X.md",
|
||||
section: "boundaries",
|
||||
item: "never-write-to-etc",
|
||||
field: "risk",
|
||||
});
|
||||
expect(m).toBeNull();
|
||||
});
|
||||
|
||||
it('R-16 field that does not match kv.key returns null', () => {
|
||||
it("R-16 field that does not match kv.key returns null", () => {
|
||||
const m = resolveOcPath(ast, {
|
||||
file: 'X.md',
|
||||
section: 'tools',
|
||||
item: 'gh',
|
||||
field: 'nonexistent',
|
||||
file: "X.md",
|
||||
section: "tools",
|
||||
item: "gh",
|
||||
field: "nonexistent",
|
||||
});
|
||||
expect(m).toBeNull();
|
||||
});
|
||||
|
||||
it('R-17 frontmatter via [frontmatter] sentinel section', () => {
|
||||
it("R-17 frontmatter via [frontmatter] sentinel section", () => {
|
||||
const m = resolveOcPath(ast, {
|
||||
file: 'X.md',
|
||||
section: '[frontmatter]',
|
||||
field: 'name',
|
||||
file: "X.md",
|
||||
section: "[frontmatter]",
|
||||
field: "name",
|
||||
});
|
||||
expect(m?.kind).toBe('frontmatter');
|
||||
if (m?.kind === 'frontmatter') {expect(m.node.value).toBe('github');}
|
||||
expect(m?.kind).toBe("frontmatter");
|
||||
if (m?.kind === "frontmatter") {
|
||||
expect(m.node.value).toBe("github");
|
||||
}
|
||||
});
|
||||
|
||||
it('R-18 frontmatter unknown key returns null', () => {
|
||||
it("R-18 frontmatter unknown key returns null", () => {
|
||||
const m = resolveOcPath(ast, {
|
||||
file: 'X.md',
|
||||
section: '[frontmatter]',
|
||||
field: 'nonexistent',
|
||||
file: "X.md",
|
||||
section: "[frontmatter]",
|
||||
field: "nonexistent",
|
||||
});
|
||||
expect(m).toBeNull();
|
||||
});
|
||||
|
||||
it('R-19 frontmatter without field returns null', () => {
|
||||
it("R-19 frontmatter without field returns null", () => {
|
||||
const m = resolveOcPath(ast, {
|
||||
file: 'X.md',
|
||||
section: '[frontmatter]',
|
||||
file: "X.md",
|
||||
section: "[frontmatter]",
|
||||
});
|
||||
expect(m).toBeNull();
|
||||
});
|
||||
|
||||
it('R-20 multiple frontmatter keys with same name — first match wins', () => {
|
||||
it("R-20 multiple frontmatter keys with same name — first match wins", () => {
|
||||
// Build an AST manually to test
|
||||
const dupeAst = {
|
||||
kind: 'md' as const,
|
||||
raw: '',
|
||||
kind: "md" as const,
|
||||
raw: "",
|
||||
frontmatter: [
|
||||
{ key: 'k', value: 'first', line: 2 },
|
||||
{ key: 'k', value: 'second', line: 3 },
|
||||
{ key: "k", value: "first", line: 2 },
|
||||
{ key: "k", value: "second", line: 3 },
|
||||
],
|
||||
preamble: '',
|
||||
preamble: "",
|
||||
blocks: [],
|
||||
};
|
||||
const m = resolveOcPath(dupeAst, {
|
||||
file: 'X.md',
|
||||
section: '[frontmatter]',
|
||||
field: 'k',
|
||||
file: "X.md",
|
||||
section: "[frontmatter]",
|
||||
field: "k",
|
||||
});
|
||||
expect(m?.kind).toBe('frontmatter');
|
||||
if (m?.kind === 'frontmatter') {expect(m.node.value).toBe('first');}
|
||||
expect(m?.kind).toBe("frontmatter");
|
||||
if (m?.kind === "frontmatter") {
|
||||
expect(m.node.value).toBe("first");
|
||||
}
|
||||
});
|
||||
|
||||
it('R-21 empty AST resolves root only', () => {
|
||||
const empty = { kind: 'md' as const, raw: '', frontmatter: [], preamble: '', blocks: [] };
|
||||
expect(resolveOcPath(empty, { file: 'X.md' })?.kind).toBe('root');
|
||||
expect(resolveOcPath(empty, { file: 'X.md', section: 'any' })).toBeNull();
|
||||
it("R-21 empty AST resolves root only", () => {
|
||||
const empty = { kind: "md" as const, raw: "", frontmatter: [], preamble: "", blocks: [] };
|
||||
expect(resolveOcPath(empty, { file: "X.md" })?.kind).toBe("root");
|
||||
expect(resolveOcPath(empty, { file: "X.md", section: "any" })).toBeNull();
|
||||
});
|
||||
|
||||
it('R-22 resolver does not mutate the AST', () => {
|
||||
it("R-22 resolver does not mutate the AST", () => {
|
||||
const before = JSON.stringify(ast);
|
||||
resolveOcPath(ast, { file: 'X.md', section: 'tools', item: 'gh', field: 'gh' });
|
||||
resolveOcPath(ast, { file: "X.md", section: "tools", item: "gh", field: "gh" });
|
||||
const after = JSON.stringify(ast);
|
||||
expect(after).toBe(before);
|
||||
});
|
||||
|
||||
it('R-23 file segment is informational — resolver doesn\'t check it', () => {
|
||||
it("R-23 file segment is informational — resolver doesn't check it", () => {
|
||||
// The file name in OcPath is metadata; resolver assumes the AST
|
||||
// matches. Callers verify file mapping before passing the AST.
|
||||
const m1 = resolveOcPath(ast, { file: 'SOUL.md', section: 'tools' });
|
||||
const m2 = resolveOcPath(ast, { file: 'AGENTS.md', section: 'tools' });
|
||||
const m1 = resolveOcPath(ast, { file: "SOUL.md", section: "tools" });
|
||||
const m2 = resolveOcPath(ast, { file: "AGENTS.md", section: "tools" });
|
||||
expect(m1?.kind).toBe(m2?.kind);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user