Skip to content
Open
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
99 changes: 99 additions & 0 deletions backend/__tests__/service/tasks.source-provenance.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
process.env.PG_HOST = '';
process.env.JWT_SECRET = 'tasks-source-provenance-test-secret';

const express = require('express');
const request = require('supertest');
const Pod = require('../../models/Pod');
const Task = require('../../models/Task');
const User = require('../../models/User');
const tasksApiRoutes = require('../../routes/tasksApi');
const { hash } = require('../../utils/secret');
const {
setupMongoDb,
closeMongoDb,
clearMongoDb,
generateTestToken,
} = require('../utils/testUtils');

const AGENT_TOKEN = 'cm_agent_task_source_provenance';

describe('POST /api/v1/tasks/:podId source provenance', () => {
let app;
let human;
let agent;
let pod;

beforeAll(async () => {
await setupMongoDb();
app = express();
app.use(express.json());
app.use('/api/v1/tasks', tasksApiRoutes);
});

beforeEach(async () => {
await clearMongoDb();
human = await User.create({
username: `task-human-${Date.now()}`,
email: `task-human-${Date.now()}@test.com`,
password: 'Password123!',
verified: true,
});
agent = await User.create({
username: `task-agent-${Date.now()}`,
email: `task-agent-${Date.now()}@agents.commonly.local`,
password: 'Password123!',
verified: true,
isBot: true,
botType: 'agent',
botMetadata: {
agentName: 'task-agent',
instanceId: 'default',
},
agentRuntimeTokens: [{
tokenHash: hash(AGENT_TOKEN),
label: 'task-source-provenance',
createdAt: new Date(),
}],
});
pod = await Pod.create({
name: 'Task provenance pod',
type: 'team',
createdBy: human._id,
members: [human._id, agent._id],
});
});

afterAll(async () => {
await clearMongoDb();
await closeMongoDb();
});

it('stamps agent-authenticated creates as agent even when the body claims human', async () => {
const response = await request(app)
.post(`/api/v1/tasks/${pod._id}`)
.set('Authorization', `Bearer ${AGENT_TOKEN}`)
.send({
title: 'Agent-authored task',
source: 'human',
})
.expect(201);

expect(response.body.task.source).toBe('agent');
const stored = await Task.findById(response.body.task._id).lean();
expect(stored.source).toBe('agent');
});

it('preserves the existing source override for human-authenticated imports', async () => {
const response = await request(app)
.post(`/api/v1/tasks/${pod._id}`)
.set('Authorization', `Bearer ${generateTestToken(human._id)}`)
.send({
title: 'Imported task',
source: 'github',
sourceRef: 'GH#999',
})
.expect(201);

expect(response.body.task.source).toBe('github');
});
});
9 changes: 8 additions & 1 deletion backend/routes/tasksApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,16 @@ router.post('/:podId', rateLimit({
if (sourceRef) initUpdate.text = `Created by ${author} from ${sourceRef}${assignee ? ` · assigned to ${assignee}` : ''}`;
if (ghNumber) initUpdate.text += ` · GH#${ghNumber}`;
if (parentTask) initUpdate.text += ` · sub-task of ${parentTask}`;
// `source` is provenance for task creation, so an agent-authenticated
// caller may not self-identify as human (or any other source) through
// the request body. Human callers retain the existing source override
// for GitHub/import workflows.
const taskSource = req.agentUser?._id
? 'agent'
: source || (ghNumber ? 'github' : 'human');
let task;
try {
task = await Task.create({ podId, taskNum, taskId, title, assignee: assignee || null, dep: dep || null, depMockOk: !!depMockOk, parentTask: parentTask || null, source: source || (ghNumber ? 'github' : 'human'), sourceRef: sourceRef || (ghNumber ? `GH#${ghNumber}` : undefined), githubIssueNumber: ghNumber, githubIssueUrl: ghUrl, updates: [initUpdate] });
task = await Task.create({ podId, taskNum, taskId, title, assignee: assignee || null, dep: dep || null, depMockOk: !!depMockOk, parentTask: parentTask || null, source: taskSource, sourceRef: sourceRef || (ghNumber ? `GH#${ghNumber}` : undefined), githubIssueNumber: ghNumber, githubIssueUrl: ghUrl, updates: [initUpdate] });
} catch (createErr) {
const duplicate = createErr as {
code?: number;
Expand Down
Loading