Files
OpenViking/examples/zcode-memory-plugin/scripts/zcode-hooks.test.mjs
T
t0saki a83b81715b feat(uri)!: remove uid-less current-user shorthand in favor of viking://~ (#4196)
* feat(uri)!: reject uid-less current-user shorthand in favor of viking://~

viking://user/<segment> (memories/resources/skills/peers/privacy/sessions
without a user id) was ambiguous with a user literally named after the
segment, and a user actually named e.g. "memories" was unreachable for
USER/ADMIN callers. Now that the viking://~ home alias (#4167) covers the
same need unambiguously, the shorthand fails closed at the request
boundary instead of expanding:

- resolve_current_user_uri raises NamespaceShapeError with a corrective
  hint naming both viking://~/<rest> and the explicit-uid form. Silently
  parsing the reserved segment as a peer user id would misdirect reads
  and writes, so rejection is the only safe removal.
- Bare viking://user falls through to the canonical parser and keeps
  container semantics (a user key listing it sees only its own space).
- The self-id escape stays: a caller whose user_id equals a reserved
  name keeps viking://user/<own-id> as their canonical root. ROOT-role
  literal parsing and the legacy viking://session alias are unchanged.
- AddTargetsConfig normalizes stored legacy config spellings
  (viking://user/resources|skills) to the viking://~ form at validation
  so existing ov.conf/user_config deployments keep working; the accepted
  per-user spelling is now viking://~/resources and viking://~/skills.
- usage_reporter keeps canonicalizing the historical shorthand found in
  old transcripts and additionally recognizes viking://~/memories/.

BREAKING CHANGE: requests using the uid-less viking://user/<segment>
spelling now fail with 400; use viking://~/<segment> or an explicit
viking://user/{user_id}/<segment> URI.

* refactor(clients): migrate first-party emitters to the viking://~ home alias

Every in-repo client that emitted the removed uid-less current-user
shorthand now sends viking://~/... instead: vikingbot fallbacks and
default sentinels, the LangChain store/tools defaults, the shared
recall-core.mjs (all synced plugin copies), the codex/claude-code/
openclaw/openwebui/dsh/zcode/pi plugin emitters, quick-app examples,
Go SDK example, tau2 benchmark targets, and the eval golden dataset.

Compat kept where legacy strings live in stored user configs: bot and
ov_dream sentinels accept both spellings while emitting only ~, and
recall-core still rewrites legacy viking://user/<reserved> config values
client-side. langchain_openviking._uri now classifies viking://~ with
the explicit-user shape so canonicalized server responses keep matching
a ~ root. Plugin READMEs note the server requirement for the alias.

* docs: replace current-user shorthand guidance with the viking://~ home alias

Rewrite every EN/ZH doc and model-facing prompt that advertised the
uid-less viking://user/<segment> spelling: URI concept catalogue,
context-types/storage/extraction/retrieval/session/privacy concepts,
configuration guide (with the legacy add_targets auto-normalization
note), resources/skills/sessions/retrieval/admin API references, FAQ,
capability reference, and the openviking-memory / ov-experience-memory /
openclaw / ov-resources skills. The stale MCP viking://user/<path>
dialect passage in the MCP guide is replaced by ~ guidance, and bare
viking://user is documented as the container of user spaces.

* test(api): migrate live API session-used tests off the removed shorthand

tests/api_test/sessions sent uid-less viking://user/skills/... URIs to
record_used, which the request boundary now rejects with 400 (caught by
the API & CLI Integration Tests CI job; these tests need a live server
and are not part of the local suites). The api_test client authenticates
as an admin-role user key, so the viking://~ home alias expands for it.
tests/api_test/common/test_edge_cases.py is left as is: it asserts a 400
for a non-resource add target, which still holds.
2026-08-21 19:00:19 +08:00

120 lines
4.5 KiB
JavaScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { evaluateZcodeUriGuard } from "./uri-guard.mjs";
const PLUGIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
// ---------------------------------------------------------------------------
// URI Guard output schema (the #1 silent-failure mode in the adversarial review)
// ZCode's strict JSON schema rejects any unrecognized key. These tests assert
// the output contains ONLY ZCode-recognized keys.
// ---------------------------------------------------------------------------
test("uri guard deny output contains only recognized keys", () => {
const output = evaluateZcodeUriGuard({
tool_name: "Read",
tool_input: { file_path: "viking://~/memories/profile.md" },
});
assert.ok(output.hookSpecificOutput);
assert.equal(output.hookSpecificOutput.hookEventName, "PreToolUse");
assert.equal(output.hookSpecificOutput.permissionDecision, "deny");
assert.ok(output.hookSpecificOutput.permissionDecisionReason);
// Must NOT contain Claude-Code-isms
assert.equal(output.decision, undefined);
assert.equal(output.hookSpecificOutput.decision, undefined);
});
test("uri guard deny reason includes MCP redirect", () => {
const output = evaluateZcodeUriGuard({
tool_name: "Read",
tool_input: { file_path: "viking://resources/myproject/docs.md" },
});
const reason = output.hookSpecificOutput?.permissionDecisionReason || "";
assert.ok(reason.includes("MCP"), "reason should mention MCP");
});
test("uri guard pass-through for non-viking URIs returns empty", () => {
const output = evaluateZcodeUriGuard({
tool_name: "Read",
tool_input: { file_path: "/tmp/local-file.txt" },
});
assert.equal(Object.keys(output).length, 0);
});
test("uri guard pass-through for Glob with local pattern", () => {
const output = evaluateZcodeUriGuard({
tool_name: "Glob",
tool_input: { pattern: "**/*.ts" },
});
assert.equal(Object.keys(output).length, 0);
});
test("uri guard deny for Glob with viking URI", () => {
const output = evaluateZcodeUriGuard({
tool_name: "Glob",
tool_input: { pattern: "viking://user/memories/**" },
});
assert.equal(output.hookSpecificOutput.permissionDecision, "deny");
});
test("uri guard handles alternative tool_input field names", () => {
const output = evaluateZcodeUriGuard({
tool_name: "Read",
toolInput: { file_path: "viking://user/test.md" },
});
assert.equal(output.hookSpecificOutput?.permissionDecision, "deny");
});
test("uri guard handles alternative tool_name field names", () => {
const output = evaluateZcodeUriGuard({
toolName: "Read",
tool_input: { file_path: "viking://user/test.md" },
});
assert.equal(output.hookSpecificOutput?.permissionDecision, "deny");
});
test("uri guard returns empty for unmatched tool name", () => {
const output = evaluateZcodeUriGuard({
tool_name: "Bash",
tool_input: { command: "cat viking://user/test.md" },
});
// Bash is not in the Read|Glob|Grep matcher — but evaluateAgentUriGuard
// may still detect viking:// in certain fields. The important assertion is
// that the output shape is valid (either empty or correct deny).
if (Object.keys(output).length > 0) {
assert.equal(output.hookSpecificOutput.hookEventName, "PreToolUse");
}
});
// ---------------------------------------------------------------------------
// Source-file conventions: hooks.json and .mcp.json must use ZCODE_PLUGIN_ROOT
// (not CLAUDE_PLUGIN_ROOT) in a .zcode-plugin/ plugin for naming consistency.
// ---------------------------------------------------------------------------
test("hooks.json uses ZCODE_PLUGIN_ROOT, not CLAUDE_PLUGIN_ROOT", () => {
const hooksJson = readFileSync(join(PLUGIN_ROOT, "hooks", "hooks.json"), "utf8");
assert.ok(
!hooksJson.includes("CLAUDE_PLUGIN_ROOT"),
"hooks.json should use ${ZCODE_PLUGIN_ROOT}, not ${CLAUDE_PLUGIN_ROOT}",
);
assert.ok(
hooksJson.includes("ZCODE_PLUGIN_ROOT"),
"hooks.json should reference ${ZCODE_PLUGIN_ROOT}",
);
});
test(".mcp.json uses ZCODE_PLUGIN_ROOT, not CLAUDE_PLUGIN_ROOT", () => {
const mcpJson = readFileSync(join(PLUGIN_ROOT, ".mcp.json"), "utf8");
assert.ok(
!mcpJson.includes("CLAUDE_PLUGIN_ROOT"),
".mcp.json should use ${ZCODE_PLUGIN_ROOT}, not ${CLAUDE_PLUGIN_ROOT}",
);
assert.ok(
mcpJson.includes("ZCODE_PLUGIN_ROOT"),
".mcp.json should reference ${ZCODE_PLUGIN_ROOT}",
);
});