Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions packages/core/src/apply/applier.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import { applyPlan } from "./applier.ts";
import { createEmptyState } from "../state/store.ts";
import { computeSkillHash } from "../execute/hash.ts";
import type { ApiClient } from "../api/interface.ts";
import type { Operation, Plan, StateFile } from "../types.ts";

Expand Down Expand Up @@ -653,6 +654,68 @@ describe("applyPlan", () => {
expect(result.failed!.name).toBe("search");
});

test("S-7: skill create hash matches planner computeSkillHash", async () => {
const files = [{ path: "SKILL.md", content: "# Skill" }];
const displayTitle = "Search Tool";
const plan: Plan = {
dependencies: {},
operations: [
{
type: "create",
resource: "skill",
name: "search",
params: { display_title: displayTitle, files },
},
],
};

const result = await applyPlan(plan, createEmptyState(), mockApiClient());
const entry = result.state.resources["skill.search"]!;
const expected = computeSkillHash(displayTitle, files);
expect(entry.last_applied_hash).toBe(expected);
});

test("S-8: skill update hash matches planner computeSkillHash", async () => {
const displayTitle = "Search Tool";
const oldFiles = [{ path: "SKILL.md", content: "# Skill" }];
const oldHash = computeSkillHash(displayTitle, oldFiles);

const state: StateFile = {
version: 1,
resources: {
"skill.search": {
type: "skill",
logical_name: "search",
id: "skill_123",
depends_on: [],
latest_version: "v1",
display_title: displayTitle,
created_at: "2026-04-20T10:00:00Z",
last_applied_hash: oldHash,
},
},
};

const newFiles = [{ path: "SKILL.md", content: "# Updated" }];
const plan: Plan = {
dependencies: {},
operations: [
{
type: "update",
resource: "skill",
name: "search",
id: "skill_123",
params: { files: newFiles },
},
],
};

const result = await applyPlan(plan, state, mockApiClient());
const entry = result.state.resources["skill.search"]!;
const expected = computeSkillHash(displayTitle, newFiles);
expect(entry.last_applied_hash).toBe(expected);
});

test("S-6: depends_on stored in state entry", async () => {
const plan: Plan = {
dependencies: {
Expand Down
12 changes: 8 additions & 4 deletions packages/core/src/apply/applier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
EnvironmentParams, SkillCreateParams, SkillUpdateParams, AgentParams,
} from "../types.ts";
import { setEntry, removeEntry } from "../state/store.ts";
import { computeHash } from "../execute/hash.ts";
import { computeHash, computeSkillHash } from "../execute/hash.ts";

export interface ApplyResult {
state: StateFile;
Expand Down Expand Up @@ -124,11 +124,11 @@ async function createResource(
apiClient: ApiClient,
): Promise<ResourceEntry> {
const now = new Date().toISOString();
const hash = computeHash(params);

switch (resource) {
case "environment": {
const typed = params as unknown as EnvironmentParams;
const hash = computeHash(params);
const result = await apiClient.environments.create(typed);
return {
type: "environment",
Expand All @@ -141,6 +141,7 @@ async function createResource(
}
case "skill": {
const typed = params as unknown as SkillCreateParams;
const hash = computeSkillHash(typed.display_title, typed.files);
const result = await apiClient.skills.create(name, typed);
return {
type: "skill",
Expand All @@ -155,6 +156,7 @@ async function createResource(
}
case "agent": {
const typed = params as unknown as AgentParams;
const hash = computeHash(params);
const result = await apiClient.agents.create(typed);
return {
type: "agent",
Expand Down Expand Up @@ -182,18 +184,19 @@ async function updateResource(
): Promise<ResourceEntry> {
const key = `${resource}.${name}`;
const existing = state.resources[key];
const hash = computeHash(params);

switch (resource) {
case "environment": {
const typed = params as unknown as EnvironmentParams;
const hash = computeHash(params);
await apiClient.environments.update(id, typed);
return { ...existing!, depends_on: dependsOn, last_applied_hash: hash } as ResourceEntry;
}
case "skill": {
const typed = params as unknown as SkillUpdateParams;
const result = await apiClient.skills.createVersion(name, id, typed);
const skillEntry = existing as SkillEntry;
const hash = computeSkillHash(skillEntry.display_title, typed.files);
const result = await apiClient.skills.createVersion(name, id, typed);
return {
...skillEntry,
latest_version: result.version_id,
Expand All @@ -202,6 +205,7 @@ async function updateResource(
}
case "agent": {
const typed = params as unknown as AgentParams;
const hash = computeHash(params);
const agentEntry = existing as AgentEntry;
const result = await apiClient.agents.update(id, {
...typed,
Expand Down
Loading