CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
task-update.test.ts354 linesDownload Raw Back to tools
1/**2 * @license3 * Copyright 2025 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';8import * as fs from 'node:fs/promises';9import * as os from 'node:os';10import * as path from 'node:path';11import { TaskUpdateTool } from './task-update.js';12import { createTask, getTask } from '../agents/team/tasks.js';13import type { ApprovalMode, Config } from '../config/config.js';14import { runWithTeammateIdentity } from '../agents/team/identity.js';15 16const DEFAULT_MODE = 'default' as ApprovalMode;17const PLAN_MODE = 'plan' as ApprovalMode;18 19vi.mock('../config/storage.js', () => {20  let mockDir = '/tmp/test';21  return {22    Storage: {23      getGlobalQwenDir: () => mockDir,24    },25    __setMockGlobalDir: (d: string) => {26      mockDir = d;27    },28  };29});30 31// eslint-disable-next-line @typescript-eslint/no-explicit-any32const { __setMockGlobalDir } = (await import('../config/storage.js')) as any;33 34let tmpDir: string;35const TEAM = 'test-team';36 37function makeConfig(approvalMode = DEFAULT_MODE) {38  return {39    getTeamContext: () => ({ teamName: TEAM }),40    getApprovalMode: () => approvalMode,41  } as unknown as Config;42}43 44beforeEach(async () => {45  tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'task-update-test-'));46  __setMockGlobalDir(tmpDir);47});48 49afterEach(async () => {50  await fs.rm(tmpDir, { recursive: true, force: true });51});52 53describe('TaskUpdateTool', () => {54  let tool: TaskUpdateTool;55 56  beforeEach(() => {57    tool = new TaskUpdateTool(makeConfig());58  });59 60  it('has the correct name', () => {61    expect(tool.name).toBe('task_update');62  });63 64  it('updates a task status', async () => {65    const task = await createTask(TEAM, {66      subject: 'Test',67      description: 'desc',68    });69    const invocation = tool.build({70      taskId: task.id,71      status: 'completed',72    });73    const result = await invocation.execute(new AbortController().signal);74    expect(result.error).toBeUndefined();75    expect(result.llmContent).toContain('completed');76  });77 78  it('deletes a task with status "deleted"', async () => {79    const task = await createTask(TEAM, {80      subject: 'Delete me',81      description: 'desc',82    });83    const invocation = tool.build({84      taskId: task.id,85      status: 'deleted',86    });87    const result = await invocation.execute(new AbortController().signal);88    expect(result.error).toBeUndefined();89    expect(result.llmContent).toContain('deleted');90  });91 92  it('returns error for non-existent task', async () => {93    const invocation = tool.build({94      taskId: '999',95      status: 'completed',96    });97    const result = await invocation.execute(new AbortController().signal);98    expect(result.error).toBeDefined();99    expect(result.llmContent).toContain('not found');100  });101 102  it('allows plan-required teammates to claim a task before approval', async () => {103    const task = await createTask(TEAM, {104      subject: 'Plan first',105      description: 'desc',106    });107    const planTool = new TaskUpdateTool(makeConfig(PLAN_MODE));108    const invocation = planTool.build({109      taskId: task.id,110      status: 'in_progress',111    });112 113    const result = await runWithTeammateIdentity(114      {115        agentName: 'planner',116        teamName: TEAM,117        agentId: 'planner@test-team',118        isTeamLead: false,119        planModeRequired: true,120      },121      () => invocation.execute(new AbortController().signal),122    );123 124    expect(result.error).toBeUndefined();125    const reloaded = await getTask(TEAM, task.id);126    expect(reloaded?.status).toBe('in_progress');127    expect(reloaded?.owner).toBe('planner');128  });129 130  it('blocks plan-required teammates from mutating tasks before approval', async () => {131    const task = await createTask(TEAM, {132      subject: 'Plan first',133      description: 'desc',134    });135    const planTool = new TaskUpdateTool(makeConfig(PLAN_MODE));136    const invocation = planTool.build({137      taskId: task.id,138      description: 'New executable instruction.',139    });140 141    const result = await runWithTeammateIdentity(142      {143        agentName: 'planner',144        teamName: TEAM,145        agentId: 'planner@test-team',146        isTeamLead: false,147        planModeRequired: true,148      },149      () => invocation.execute(new AbortController().signal),150    );151 152    expect(result.error).toBeDefined();153    expect(result.llmContent).toContain('waiting for leader approval');154    const reloaded = await getTask(TEAM, task.id);155    expect(reloaded?.description).toBe('desc');156  });157 158  it('does not let plan-required teammates reclaim non-pending tasks before approval', async () => {159    const task = await createTask(TEAM, {160      subject: 'Completed',161      description: 'desc',162    });163    await tool164      .build({ taskId: task.id, status: 'completed' })165      .execute(new AbortController().signal);166 167    const planTool = new TaskUpdateTool(makeConfig(PLAN_MODE));168    const result = await runWithTeammateIdentity(169      {170        agentName: 'planner',171        teamName: TEAM,172        agentId: 'planner@test-team',173        isTeamLead: false,174        planModeRequired: true,175      },176      () =>177        planTool178          .build({ taskId: task.id, status: 'in_progress' })179          .execute(new AbortController().signal),180    );181 182    expect(result.error).toBeDefined();183    expect(result.llmContent).toContain('unowned pending task');184    const reloaded = await getTask(TEAM, task.id);185    expect(reloaded?.status).toBe('completed');186  });187 188  it('validates required taskId', () => {189    expect(() => tool.build({} as never)).toThrow();190  });191 192  it('rejects addBlockedBy that references a missing task', async () => {193    const task = await createTask(TEAM, {194      subject: 'Test',195      description: 'desc',196    });197    const invocation = tool.build({198      taskId: task.id,199      addBlockedBy: ['999'],200    });201    const result = await invocation.execute(new AbortController().signal);202    expect(result.error).toBeDefined();203    expect(result.llmContent).toContain('not found');204    expect(result.llmContent).toContain('#999');205 206    // Primary task must remain unchanged when validation fails so207    // the model can retry with a real id.208    const { getTask } = await import('../agents/team/tasks.js');209    const reloaded = await getTask(TEAM, task.id);210    expect(reloaded?.blockedBy ?? []).toEqual([]);211  });212 213  it('rejects addBlocks that references a missing task', async () => {214    const task = await createTask(TEAM, {215      subject: 'Test',216      description: 'desc',217    });218    const invocation = tool.build({219      taskId: task.id,220      addBlocks: ['999'],221    });222    const result = await invocation.execute(new AbortController().signal);223    expect(result.error).toBeDefined();224    expect(result.llmContent).toContain('#999');225  });226 227  it('mirrors dependency edges when both ids exist', async () => {228    const a = await createTask(TEAM, { subject: 'A', description: 'a' });229    const b = await createTask(TEAM, { subject: 'B', description: 'b' });230    const invocation = tool.build({231      taskId: a.id,232      addBlockedBy: [b.id],233    });234    const result = await invocation.execute(new AbortController().signal);235    expect(result.error).toBeUndefined();236 237    const { getTask } = await import('../agents/team/tasks.js');238    const aReloaded = await getTask(TEAM, a.id);239    const bReloaded = await getTask(TEAM, b.id);240    expect(aReloaded?.blockedBy).toContain(b.id);241    expect(bReloaded?.blocks).toContain(a.id);242  });243 244  it('does not re-block a dependent when completing with addBlocks in the same call', async () => {245    // Regression (verified repro): task_update({ status:'completed',246    // addBlocks:['2'] }) merged the edge, ran completion-unblock (a247    // no-op because the reciprocal blockedBy didn't exist yet), then the248    // addBlocks reciprocal added blockedBy:['1'] back — leaving task 2249    // permanently blocked by the already-completed task 1, so auto-claim250    // would never pick it up. The tool now skips the addBlocks reciprocal251    // when the same call completes the task.252    const a = await createTask(TEAM, { subject: 'A', description: 'a' });253    const b = await createTask(TEAM, { subject: 'B', description: 'b' });254 255    const invocation = tool.build({256      taskId: a.id,257      status: 'completed',258      addBlocks: [b.id],259    });260    const result = await invocation.execute(new AbortController().signal);261    expect(result.error).toBeUndefined();262 263    const { getTask } = await import('../agents/team/tasks.js');264    const aReloaded = await getTask(TEAM, a.id);265    const bReloaded = await getTask(TEAM, b.id);266    expect(aReloaded?.status).toBe('completed');267    // The completed blocker must leave b claimable, not blocked.268    expect(bReloaded?.blockedBy ?? []).toEqual([]);269  });270 271  it('rejects a self-edge', async () => {272    // A task blocked by itself can never be auto-claimed (non-empty273    // blockedBy) and can never complete to unblock itself — a silent274    // permanent deadlock if accepted.275    const task = await createTask(TEAM, { subject: 'T', description: 'd' });276    const invocation = tool.build({277      taskId: task.id,278      addBlockedBy: [task.id],279    });280    const result = await invocation.execute(new AbortController().signal);281    expect(result.error).toBeDefined();282    expect(result.llmContent).toContain('itself');283 284    const { getTask } = await import('../agents/team/tasks.js');285    const reloaded = await getTask(TEAM, task.id);286    expect(reloaded?.blockedBy ?? []).toEqual([]);287  });288 289  it('rejects an edge that closes a dependency cycle', async () => {290    const a = await createTask(TEAM, { subject: 'A', description: 'a' });291    const b = await createTask(TEAM, { subject: 'B', description: 'b' });292    const c = await createTask(TEAM, { subject: 'C', description: 'c' });293 294    // a → b → c (blocks direction), then closing c → a must fail.295    let result = await tool296      .build({ taskId: b.id, addBlockedBy: [a.id] })297      .execute(new AbortController().signal);298    expect(result.error).toBeUndefined();299    result = await tool300      .build({ taskId: c.id, addBlockedBy: [b.id] })301      .execute(new AbortController().signal);302    expect(result.error).toBeUndefined();303 304    result = await tool305      .build({ taskId: a.id, addBlockedBy: [c.id] })306      .execute(new AbortController().signal);307    expect(result.error).toBeDefined();308    expect(result.llmContent).toContain('cycle');309 310    // The rejected edge must not be half-persisted.311    const { getTask } = await import('../agents/team/tasks.js');312    const aReloaded = await getTask(TEAM, a.id);313    expect(aReloaded?.blockedBy ?? []).toEqual([]);314  });315 316  // ─── Permission surface ───────────────────────────────────317  // Mirrors task-create: a regression back to 'allow' or the base ''318  // classifier sentinel re-opens the instruction-rewrite path.319 320  it("defaults to 'ask' permission", async () => {321    const invocation = tool.build({ taskId: '1', status: 'completed' });322    await expect(invocation.getDefaultPermission()).resolves.toBe('ask');323  });324 325  it('projects the mutating fields to the AUTO classifier', () => {326    const projected = tool.toAutoClassifierInput({327      taskId: '1',328      status: 'in_progress',329      owner: 'worker',330      description: 'rewritten instruction',331    });332    expect(projected).toMatchObject({333      taskId: '1',334      status: 'in_progress',335      owner: 'worker',336      description: 'rewritten instruction',337    });338  });339 340  it('shows an updated description in the confirmation prompt', async () => {341    const invocation = tool.build({342      taskId: '7',343      description: 'New instruction text the teammate will execute.',344    });345    const details = await invocation.getConfirmationDetails(346      new AbortController().signal,347    );348    expect(details.type).toBe('info');349    expect((details as { prompt: string }).prompt).toContain(350      'New instruction text the teammate will execute.',351    );352  });353});354 
basant307/AI_Governance_Project · CoolFace