Phase C of vendor-agnostic model-access (ADR 0050/0054). Two TypeScript runtime modules: - anthropic-manager: the refresh token is sealed at rest to the manager node's own key (atrest.ts, envelope encryption over X25519) and opened ONLY on the manager node. adopt seals the first envelope; refresh opens it, calls the Anthropic OAuth token endpoint, re-seals a rotated refresh token, and hands the control plane only the access token plus the opaque envelope. Also polls licence-grain usage (ADR 0054). - anthropic-consumer: writes the delivered access token to ~/.claude/.credentials.json, access-token-only, atomically (the refresh token is never delivered); reports session-grain usage from the CLI transcripts; a fail-closed identity guard (expected-uuid plumbing is a flagged TODO). Both run as scheduled containers (ADR 0053). Pure logic covered by node --test fixtures (at-rest round-trip, credential strip, transcript sum, refresh merge). Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
49 lines
1.8 KiB
TypeScript
49 lines
1.8 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { foldTranscript } from "../transcript.ts";
|
|
|
|
// A captured-shape transcript: two assistant turns and a user line, exactly the fields the port map
|
|
// names. Not imagined — the field names match the mature implementation's parse.
|
|
const TRANSCRIPT = [
|
|
JSON.stringify({ type: "user", timestamp: "2026-01-01T00:00:00Z", cwd: "/work/app", gitBranch: "main" }),
|
|
JSON.stringify({
|
|
type: "assistant",
|
|
timestamp: "2026-01-01T00:00:01Z",
|
|
costUSD: 0.01,
|
|
message: {
|
|
model: "claude-opus-4-8",
|
|
usage: { input_tokens: 100, cache_creation_input_tokens: 20, cache_read_input_tokens: 5, output_tokens: 40 },
|
|
},
|
|
}),
|
|
JSON.stringify({
|
|
type: "assistant",
|
|
timestamp: "2026-01-01T00:00:02Z",
|
|
costUSD: 0.02,
|
|
message: { model: "claude-opus-4-8", usage: { input_tokens: 200, output_tokens: 60 } },
|
|
}),
|
|
"", // a half-written trailing line is ordinary and must not be fatal.
|
|
].join("\n");
|
|
|
|
test("a transcript sums per-session token counts, cost, and metadata", () => {
|
|
const s = foldTranscript("session-abc", TRANSCRIPT);
|
|
assert.equal(s.sessionId, "session-abc");
|
|
assert.equal(s.turns, 2);
|
|
assert.equal(s.inputTokens, 300);
|
|
assert.equal(s.cacheCreationTokens, 20);
|
|
assert.equal(s.cacheReadTokens, 5);
|
|
assert.equal(s.outputTokens, 100);
|
|
assert.equal(Math.round(s.costUSD * 100) / 100, 0.03);
|
|
assert.equal(s.model, "claude-opus-4-8");
|
|
assert.equal(s.gitBranch, "main");
|
|
assert.equal(s.cwd, "/work/app");
|
|
assert.equal(s.startedAt, "2026-01-01T00:00:00Z");
|
|
assert.equal(s.lastActive, "2026-01-01T00:00:02Z");
|
|
});
|
|
|
|
test("a malformed line is skipped, not fatal", () => {
|
|
const s = foldTranscript("s", 'not json\n{"type":"assistant","message":{"usage":{"output_tokens":7}}}');
|
|
assert.equal(s.outputTokens, 7);
|
|
assert.equal(s.turns, 1);
|
|
});
|